mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
Merge pull request #1431 from eyaltoledano/next (Release 0.34.0)
This commit is contained in:
5
.changeset/rich-kings-fold.md
Normal file
5
.changeset/rich-kings-fold.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Deprecated generate command
|
||||
@@ -32,7 +32,6 @@ task-master expand --all --research # Expand all eligible tasks
|
||||
task-master add-dependency --id=<id> --depends-on=<id> # Add task dependency
|
||||
task-master move --from=<id> --to=<id> # Reorganize task hierarchy
|
||||
task-master validate-dependencies # Check for dependency issues
|
||||
task-master generate # Update task markdown files (usually auto-called)
|
||||
```
|
||||
|
||||
## Key Files & Project Structure
|
||||
@@ -361,9 +360,6 @@ task-master models --set-fallback gpt-4o-mini
|
||||
### Task File Sync Issues
|
||||
|
||||
```bash
|
||||
# Regenerate task files from tasks.json
|
||||
task-master generate
|
||||
|
||||
# Fix dependency issues
|
||||
task-master fix-dependencies
|
||||
```
|
||||
@@ -389,8 +385,6 @@ These commands make AI calls and may take up to a minute:
|
||||
|
||||
- Never manually edit `tasks.json` - use commands instead
|
||||
- Never manually edit `.taskmaster/config.json` - use `task-master models`
|
||||
- Task markdown files in `tasks/` are auto-generated
|
||||
- Run `task-master generate` after manual changes to tasks.json
|
||||
|
||||
### Claude Code Session Management
|
||||
|
||||
|
||||
@@ -277,9 +277,6 @@ task-master move --from=5 --from-tag=backlog --to-tag=in-progress
|
||||
task-master move --from=5,6,7 --from-tag=backlog --to-tag=done --with-dependencies
|
||||
task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies
|
||||
|
||||
# Generate task files
|
||||
task-master generate
|
||||
|
||||
# Add rules after initialization
|
||||
task-master rules add windsurf,roo,vscode
|
||||
```
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"tsx": "^4.20.4",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import boxen from 'boxen';
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Level/variant for the card box styling
|
||||
*/
|
||||
export type CardBoxLevel = 'warn' | 'info';
|
||||
|
||||
/**
|
||||
* Configuration for the card box component
|
||||
*/
|
||||
export interface CardBoxConfig {
|
||||
/** Header text displayed in yellow bold */
|
||||
/** Header text displayed in bold */
|
||||
header: string;
|
||||
/** Body paragraphs displayed in white */
|
||||
body: string[];
|
||||
/** Call to action section with label and URL */
|
||||
callToAction: {
|
||||
/** Call to action section with label and URL (optional) */
|
||||
callToAction?: {
|
||||
label: string;
|
||||
action: string;
|
||||
};
|
||||
/** Footer text displayed in gray (usage instructions) */
|
||||
footer?: string;
|
||||
/** Level/variant for styling (default: 'warn' = yellow, 'info' = blue) */
|
||||
level?: CardBoxLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,22 +33,30 @@ export interface CardBoxConfig {
|
||||
* @returns Formatted string ready for console.log
|
||||
*/
|
||||
export function displayCardBox(config: CardBoxConfig): string {
|
||||
const { header, body, callToAction, footer } = config;
|
||||
const { header, body, callToAction, footer, level = 'warn' } = config;
|
||||
|
||||
// Determine colors based on level
|
||||
const headerColor = level === 'info' ? chalk.blue.bold : chalk.yellow.bold;
|
||||
const borderColor = level === 'info' ? 'blue' : 'yellow';
|
||||
|
||||
// Build the content sections
|
||||
const sections: string[] = [
|
||||
// Header
|
||||
chalk.yellow.bold(header),
|
||||
headerColor(header),
|
||||
|
||||
// Body paragraphs
|
||||
...body.map((paragraph) => chalk.white(paragraph)),
|
||||
|
||||
// Call to action
|
||||
chalk.cyan(callToAction.label) +
|
||||
'\n' +
|
||||
chalk.blue.underline(callToAction.action)
|
||||
...body.map((paragraph) => chalk.white(paragraph))
|
||||
];
|
||||
|
||||
// Add call to action if provided
|
||||
if (callToAction && callToAction.label && callToAction.action) {
|
||||
sections.push(
|
||||
chalk.cyan(callToAction.label) +
|
||||
'\n' +
|
||||
chalk.blue.underline(callToAction.action)
|
||||
);
|
||||
}
|
||||
|
||||
// Add footer if provided
|
||||
if (footer) {
|
||||
sections.push(chalk.gray(footer));
|
||||
@@ -53,7 +68,7 @@ export function displayCardBox(config: CardBoxConfig): string {
|
||||
// Wrap in boxen
|
||||
return boxen(content, {
|
||||
padding: 1,
|
||||
borderColor: 'yellow',
|
||||
borderColor,
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 1 }
|
||||
});
|
||||
|
||||
144
apps/cli/src/utils/command-guard.ts
Normal file
144
apps/cli/src/utils/command-guard.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @fileoverview Command guard for CLI commands
|
||||
* CLI presentation layer - uses tm-core for logic, displays with cardBox
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { createTmCore, type TmCore, type LocalOnlyCommand } from '@tm/core';
|
||||
import { displayCardBox } from '../ui/components/cardBox.component.js';
|
||||
|
||||
/**
|
||||
* Command-specific messaging configuration
|
||||
*/
|
||||
interface CommandMessage {
|
||||
header: string;
|
||||
getBody: (briefName: string) => string[];
|
||||
footer: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command-specific message configuration
|
||||
*
|
||||
* NOTE: Command groups below are intentionally hardcoded (not imported from LOCAL_ONLY_COMMANDS)
|
||||
* to allow flexible categorization with custom messaging per category. All commands here are
|
||||
* subsets of LOCAL_ONLY_COMMANDS from @tm/core, which is the source of truth for blocked commands.
|
||||
*
|
||||
* Categories exist for UX purposes (tailored messaging), while LOCAL_ONLY_COMMANDS exists for
|
||||
* enforcement (what's actually blocked when using Hamster).
|
||||
*/
|
||||
function getCommandMessage(commandName: LocalOnlyCommand): CommandMessage {
|
||||
// Dependency management commands
|
||||
if (
|
||||
[
|
||||
'add-dependency',
|
||||
'remove-dependency',
|
||||
'validate-dependencies',
|
||||
'fix-dependencies'
|
||||
].includes(commandName)
|
||||
) {
|
||||
return {
|
||||
header: 'Hamster Manages Dependencies',
|
||||
getBody: (briefName) => [
|
||||
`Hamster handles dependencies for the ${chalk.blue(`"${briefName}"`)} Brief.`,
|
||||
`To manage dependencies manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
|
||||
],
|
||||
footer:
|
||||
'Switch between local and remote workflows anytime by logging in/out.'
|
||||
};
|
||||
}
|
||||
|
||||
// Subtask management commands
|
||||
if (commandName === 'clear-subtasks') {
|
||||
return {
|
||||
header: 'Hamster Manages Subtasks',
|
||||
getBody: (briefName) => [
|
||||
`Hamster handles subtask management for the ${chalk.blue(`"${briefName}"`)} Brief.`,
|
||||
`To manage subtasks manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
|
||||
],
|
||||
footer:
|
||||
'Switch between local and remote workflows anytime by logging in/out.'
|
||||
};
|
||||
}
|
||||
|
||||
// Model configuration commands
|
||||
if (commandName === 'models') {
|
||||
return {
|
||||
header: 'Hamster Manages AI Models',
|
||||
getBody: (briefName) => [
|
||||
`Hamster configures AI models automatically for the ${chalk.blue(`"${briefName}"`)} Brief.`,
|
||||
`To configure models manually, log out with ${chalk.cyan('tm auth logout')} and work locally.`
|
||||
],
|
||||
footer:
|
||||
'Switch between local and remote workflows anytime by logging in/out.'
|
||||
};
|
||||
}
|
||||
|
||||
// Default message for any other local-only commands
|
||||
return {
|
||||
header: 'Command Not Available in Hamster',
|
||||
getBody: (briefName) => [
|
||||
`The ${chalk.cyan(commandName)} command is managed by Hamster for the ${chalk.blue(`"${briefName}"`)} Brief.`,
|
||||
`To use this command, log out with ${chalk.cyan('tm auth logout')} and work locally.`
|
||||
],
|
||||
footer:
|
||||
'Switch between local and remote workflows anytime by logging in/out.'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command should be blocked when authenticated and display CLI message
|
||||
*
|
||||
* Use this for CLI commands that are only available for local file storage (not Hamster).
|
||||
* This uses tm-core's AuthDomain.guardCommand() and formats the result for CLI display.
|
||||
*
|
||||
* @param commandName - Name of the command being executed
|
||||
* @param tmCoreOrProjectRoot - TmCore instance or project root path
|
||||
* @returns true if command should be blocked, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const isBlocked = await checkAndBlockIfAuthenticated('add-dependency', projectRoot);
|
||||
* if (isBlocked) {
|
||||
* process.exit(1);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function checkAndBlockIfAuthenticated(
|
||||
commandName: string,
|
||||
tmCoreOrProjectRoot: TmCore | string
|
||||
): Promise<boolean> {
|
||||
// Get or create TmCore instance
|
||||
const tmCore =
|
||||
typeof tmCoreOrProjectRoot === 'string'
|
||||
? await createTmCore({ projectPath: tmCoreOrProjectRoot })
|
||||
: tmCoreOrProjectRoot;
|
||||
|
||||
// Use tm-core's auth domain to check the command guard
|
||||
const result = await tmCore.auth.guardCommand(
|
||||
commandName,
|
||||
tmCore.tasks.getStorageType()
|
||||
);
|
||||
|
||||
if (result.isBlocked) {
|
||||
// Get command-specific message configuration
|
||||
// Safe to cast: guardCommand only blocks commands in LOCAL_ONLY_COMMANDS
|
||||
const message = getCommandMessage(commandName as LocalOnlyCommand);
|
||||
const briefName = result.briefName || 'remote brief';
|
||||
|
||||
// Format and display CLI message with cardBox
|
||||
console.log(
|
||||
displayCardBox({
|
||||
header: message.header,
|
||||
body: message.getBody(briefName),
|
||||
footer: message.footer,
|
||||
level: 'info'
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const checkAndBlockDependencyCommand = checkAndBlockIfAuthenticated;
|
||||
@@ -12,6 +12,12 @@ export {
|
||||
type CheckAuthOptions
|
||||
} from './auth-helpers.js';
|
||||
|
||||
// Command guard for local-only commands
|
||||
export {
|
||||
checkAndBlockIfAuthenticated,
|
||||
checkAndBlockDependencyCommand // Legacy export
|
||||
} from './command-guard.js';
|
||||
|
||||
// Error handling utilities
|
||||
export { displayError, isDebugMode } from './error-handler.js';
|
||||
|
||||
|
||||
307
apps/cli/tests/fixtures/task-fixtures.ts
vendored
Normal file
307
apps/cli/tests/fixtures/task-fixtures.ts
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* @fileoverview Test fixtures for creating valid task data structures
|
||||
*
|
||||
* WHY FIXTURES:
|
||||
* - Ensures all required fields are present (prevents validation errors)
|
||||
* - Provides consistent, realistic test data
|
||||
* - Easy to override specific fields for test scenarios
|
||||
* - Single source of truth for valid task structures
|
||||
*
|
||||
* USAGE:
|
||||
* ```ts
|
||||
* import { createTask, createTasksFile } from '../fixtures/task-fixtures';
|
||||
*
|
||||
* // Create a single task with defaults
|
||||
* const task = createTask({ id: 1, title: 'My Task', status: 'pending' });
|
||||
*
|
||||
* // Create a complete tasks.json structure
|
||||
* const tasksFile = createTasksFile({
|
||||
* tasks: [
|
||||
* createTask({ id: 1, title: 'Task 1' }),
|
||||
* createTask({ id: 2, title: 'Task 2', dependencies: ['1'] })
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { Task, Subtask, TaskMetadata } from '@tm/core';
|
||||
|
||||
/**
|
||||
* File structure for tasks.json
|
||||
* Note: Uses the 'master' tag as the default tag name
|
||||
*/
|
||||
export interface TasksFile {
|
||||
master: {
|
||||
tasks: Task[];
|
||||
metadata: TaskMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid task with all required fields
|
||||
*
|
||||
* DEFAULTS:
|
||||
* - id: Converted to string if number is provided
|
||||
* - status: 'pending'
|
||||
* - priority: 'medium'
|
||||
* - dependencies: []
|
||||
* - subtasks: []
|
||||
* - description: Same as title
|
||||
* - details: Empty string
|
||||
* - testStrategy: Empty string
|
||||
*/
|
||||
export function createTask(
|
||||
overrides: Partial<Omit<Task, 'id'>> & { id: number | string; title: string }
|
||||
): Task {
|
||||
return {
|
||||
id: String(overrides.id),
|
||||
title: overrides.title,
|
||||
description: overrides.description ?? overrides.title,
|
||||
status: overrides.status ?? 'pending',
|
||||
priority: overrides.priority ?? 'medium',
|
||||
dependencies: overrides.dependencies ?? [],
|
||||
details: overrides.details ?? '',
|
||||
testStrategy: overrides.testStrategy ?? '',
|
||||
subtasks: overrides.subtasks ?? [],
|
||||
// Spread any additional optional fields
|
||||
...(overrides.createdAt && { createdAt: overrides.createdAt }),
|
||||
...(overrides.updatedAt && { updatedAt: overrides.updatedAt }),
|
||||
...(overrides.effort && { effort: overrides.effort }),
|
||||
...(overrides.actualEffort && { actualEffort: overrides.actualEffort }),
|
||||
...(overrides.tags && { tags: overrides.tags }),
|
||||
...(overrides.assignee && { assignee: overrides.assignee }),
|
||||
...(overrides.databaseId && { databaseId: overrides.databaseId }),
|
||||
...(overrides.complexity && { complexity: overrides.complexity }),
|
||||
...(overrides.recommendedSubtasks && {
|
||||
recommendedSubtasks: overrides.recommendedSubtasks
|
||||
}),
|
||||
...(overrides.expansionPrompt && {
|
||||
expansionPrompt: overrides.expansionPrompt
|
||||
}),
|
||||
...(overrides.complexityReasoning && {
|
||||
complexityReasoning: overrides.complexityReasoning
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid subtask with all required fields
|
||||
*
|
||||
* DEFAULTS:
|
||||
* - id: Can be number or string
|
||||
* - status: 'pending'
|
||||
* - priority: 'medium'
|
||||
* - dependencies: []
|
||||
* - description: Same as title
|
||||
* - details: Empty string
|
||||
* - testStrategy: Empty string
|
||||
* - parentId: Derived from id if not provided (e.g., '1.2' -> parentId '1')
|
||||
*/
|
||||
export function createSubtask(
|
||||
overrides: Partial<Omit<Subtask, 'id' | 'parentId'>> & {
|
||||
id: number | string;
|
||||
title: string;
|
||||
parentId?: string;
|
||||
}
|
||||
): Subtask {
|
||||
const idStr = String(overrides.id);
|
||||
const defaultParentId = idStr.includes('.') ? idStr.split('.')[0] : '1';
|
||||
|
||||
return {
|
||||
id: overrides.id,
|
||||
parentId: overrides.parentId ?? defaultParentId,
|
||||
title: overrides.title,
|
||||
description: overrides.description ?? overrides.title,
|
||||
status: overrides.status ?? 'pending',
|
||||
priority: overrides.priority ?? 'medium',
|
||||
dependencies: overrides.dependencies ?? [],
|
||||
details: overrides.details ?? '',
|
||||
testStrategy: overrides.testStrategy ?? '',
|
||||
// Spread any additional optional fields
|
||||
...(overrides.createdAt && { createdAt: overrides.createdAt }),
|
||||
...(overrides.updatedAt && { updatedAt: overrides.updatedAt }),
|
||||
...(overrides.effort && { effort: overrides.effort }),
|
||||
...(overrides.actualEffort && { actualEffort: overrides.actualEffort }),
|
||||
...(overrides.tags && { tags: overrides.tags }),
|
||||
...(overrides.assignee && { assignee: overrides.assignee }),
|
||||
...(overrides.databaseId && { databaseId: overrides.databaseId }),
|
||||
...(overrides.complexity && { complexity: overrides.complexity }),
|
||||
...(overrides.recommendedSubtasks && {
|
||||
recommendedSubtasks: overrides.recommendedSubtasks
|
||||
}),
|
||||
...(overrides.expansionPrompt && {
|
||||
expansionPrompt: overrides.expansionPrompt
|
||||
}),
|
||||
...(overrides.complexityReasoning && {
|
||||
complexityReasoning: overrides.complexityReasoning
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete tasks.json file structure
|
||||
*
|
||||
* DEFAULTS:
|
||||
* - Empty tasks array
|
||||
* - version: '1.0.0'
|
||||
* - lastModified: Current timestamp
|
||||
* - taskCount: Calculated from tasks array
|
||||
* - completedCount: Calculated from tasks array
|
||||
* - description: 'Test tasks'
|
||||
*/
|
||||
export function createTasksFile(overrides?: {
|
||||
tasks?: Task[];
|
||||
metadata?: Partial<TaskMetadata>;
|
||||
}): TasksFile {
|
||||
const tasks = overrides?.tasks ?? [];
|
||||
const completedTasks = tasks.filter(
|
||||
(t) =>
|
||||
t.status === 'done' ||
|
||||
t.status === 'completed' ||
|
||||
t.status === 'cancelled'
|
||||
);
|
||||
|
||||
const defaultMetadata: TaskMetadata = {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount: completedTasks.length,
|
||||
description: 'Test tasks',
|
||||
...overrides?.metadata
|
||||
};
|
||||
|
||||
return {
|
||||
master: {
|
||||
tasks,
|
||||
metadata: defaultMetadata
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-built task scenarios for common test cases
|
||||
*/
|
||||
export const TaskScenarios = {
|
||||
/**
|
||||
* Single pending task with no dependencies
|
||||
*/
|
||||
simplePendingTask: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Simple Task',
|
||||
description: 'A basic pending task'
|
||||
})
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Linear dependency chain: 1 -> 2 -> 3
|
||||
*/
|
||||
linearDependencyChain: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Step 1', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Step 2',
|
||||
status: 'done',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Step 3',
|
||||
status: 'pending',
|
||||
dependencies: ['2']
|
||||
})
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Tasks with mixed statuses
|
||||
*/
|
||||
mixedStatuses: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done Task', status: 'done' }),
|
||||
createTask({ id: 2, title: 'In Progress Task', status: 'in-progress' }),
|
||||
createTask({ id: 3, title: 'Pending Task', status: 'pending' }),
|
||||
createTask({ id: 4, title: 'Review Task', status: 'review' })
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Task with subtasks
|
||||
*/
|
||||
taskWithSubtasks: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
status: 'in-progress',
|
||||
subtasks: [
|
||||
createSubtask({ id: '1.1', title: 'Subtask 1', status: 'done' }),
|
||||
createSubtask({
|
||||
id: '1.2',
|
||||
title: 'Subtask 2',
|
||||
status: 'in-progress',
|
||||
dependencies: ['1.1']
|
||||
}),
|
||||
createSubtask({
|
||||
id: '1.3',
|
||||
title: 'Subtask 3',
|
||||
status: 'pending',
|
||||
dependencies: ['1.2']
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Complex dependency graph with multiple paths
|
||||
*/
|
||||
complexDependencies: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Foundation', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Build A',
|
||||
status: 'done',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Build B',
|
||||
status: 'done',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 4,
|
||||
title: 'Integration',
|
||||
status: 'pending',
|
||||
dependencies: ['2', '3']
|
||||
})
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* All tasks completed (for testing "no next task" scenario)
|
||||
*/
|
||||
allCompleted: () =>
|
||||
createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done 1', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Done 2', status: 'done' }),
|
||||
createTask({ id: 3, title: 'Done 3', status: 'done' })
|
||||
]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Empty task list
|
||||
*/
|
||||
empty: () => createTasksFile({ tasks: [] })
|
||||
};
|
||||
19
apps/cli/tests/helpers/test-utils.ts
Normal file
19
apps/cli/tests/helpers/test-utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @fileoverview Shared test utilities for integration tests
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Get the absolute path to the compiled CLI binary
|
||||
*
|
||||
* IMPORTANT: This resolves to the root dist/ directory, not apps/cli/dist/
|
||||
* The CLI is built to <repo-root>/dist/task-master.js
|
||||
*
|
||||
* @returns Absolute path to task-master.js binary
|
||||
*/
|
||||
export function getCliBinPath(): string {
|
||||
// From apps/cli/tests/helpers/ navigate to repo root
|
||||
const repoRoot = path.resolve(__dirname, '../../../..');
|
||||
return path.join(repoRoot, 'dist', 'task-master.js');
|
||||
}
|
||||
428
apps/cli/tests/integration/commands/list.command.test.ts
Normal file
428
apps/cli/tests/integration/commands/list.command.test.ts
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for 'task-master list' command
|
||||
*
|
||||
* Tests the list command which displays all tasks with optional filtering.
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createSubtask,
|
||||
createTask,
|
||||
createTasksFile
|
||||
} from '../../fixtures/task-fixtures';
|
||||
import { getCliBinPath } from '../../helpers/test-utils';
|
||||
|
||||
// Capture initial working directory at module load time
|
||||
const initialCwd = process.cwd();
|
||||
|
||||
describe('list command', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let binPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-list-test-'));
|
||||
process.chdir(testDir);
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
binPath = getCliBinPath();
|
||||
|
||||
execSync(`node "${binPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Use fixture to create initial empty tasks file
|
||||
const initialTasks = createTasksFile();
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
// Restore to the original working directory captured at module load
|
||||
process.chdir(initialCwd);
|
||||
} catch {
|
||||
// Fallback to home directory if initial directory no longer exists
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
const runList = (args = ''): { output: string; exitCode: number } => {
|
||||
try {
|
||||
const output = execSync(`node "${binPath}" list ${args}`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
return { output, exitCode: 0 };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
output: error.stderr?.toString() || error.stdout?.toString() || '',
|
||||
exitCode: error.status || 1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
it('should display message when no tasks exist', () => {
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('No tasks found');
|
||||
});
|
||||
|
||||
it('should list all tasks with correct information', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Setup Environment',
|
||||
description: 'Install and configure',
|
||||
status: 'done',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Write Tests',
|
||||
description: 'Create test suite',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Implement Feature',
|
||||
description: 'Build the thing',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: ['2']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Setup Environment');
|
||||
expect(output).toContain('Write Tests');
|
||||
expect(output).toContain('Implement Feature');
|
||||
});
|
||||
|
||||
it('should display task statuses', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Done Task',
|
||||
status: 'done',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Pending Task',
|
||||
status: 'pending',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'In Progress',
|
||||
status: 'in-progress',
|
||||
priority: 'high'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
// Should show status indicators (exact format may vary)
|
||||
expect(output.toLowerCase()).toContain('done');
|
||||
expect(output.toLowerCase()).toContain('pending');
|
||||
expect(output.toLowerCase()).toContain('progress');
|
||||
});
|
||||
|
||||
it('should show subtasks when --with-subtasks flag is used', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
status: 'in-progress',
|
||||
subtasks: [
|
||||
createSubtask({
|
||||
id: '1.1',
|
||||
title: 'First Subtask',
|
||||
status: 'done',
|
||||
parentId: '1'
|
||||
}),
|
||||
createSubtask({
|
||||
id: '1.2',
|
||||
title: 'Second Subtask',
|
||||
status: 'pending',
|
||||
parentId: '1'
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList('--with-subtasks');
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Parent Task');
|
||||
expect(output).toContain('First Subtask');
|
||||
expect(output).toContain('Second Subtask');
|
||||
});
|
||||
|
||||
it('should handle tasks with dependencies', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Foundation', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Dependent Task',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Foundation');
|
||||
expect(output).toContain('Dependent Task');
|
||||
});
|
||||
|
||||
it('should display multiple tasks in order', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Task One', status: 'pending' }),
|
||||
createTask({ id: 2, title: 'Task Two', status: 'pending' }),
|
||||
createTask({ id: 3, title: 'Task Three', status: 'pending' }),
|
||||
createTask({ id: 4, title: 'Task Four', status: 'pending' }),
|
||||
createTask({ id: 5, title: 'Task Five', status: 'pending' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Task One');
|
||||
expect(output).toContain('Task Two');
|
||||
expect(output).toContain('Task Three');
|
||||
expect(output).toContain('Task Four');
|
||||
expect(output).toContain('Task Five');
|
||||
});
|
||||
|
||||
describe('error handling - validation errors should surface to CLI', () => {
|
||||
it('should display validation error when task has missing description', () => {
|
||||
// Create intentionally invalid task data bypassing fixtures
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Invalid Task',
|
||||
description: '', // ❌ Invalid - empty description
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 1,
|
||||
completedCount: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(output.toLowerCase()).toContain('description');
|
||||
expect(output.toLowerCase()).toContain('required');
|
||||
// Should NOT contain the generic wrapped error message
|
||||
expect(output).not.toContain('Failed to get task list');
|
||||
});
|
||||
|
||||
it('should display validation error when task has missing title', () => {
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: '', // ❌ Invalid - empty title
|
||||
description: 'A task without a title',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 1,
|
||||
completedCount: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(output.toLowerCase()).toContain('title');
|
||||
expect(output.toLowerCase()).toContain('required');
|
||||
expect(output).not.toContain('Failed to get task list');
|
||||
});
|
||||
|
||||
it('should display validation error when task has only whitespace in description', () => {
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task with whitespace description',
|
||||
description: ' ', // ❌ Invalid - only whitespace
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 1,
|
||||
completedCount: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(output.toLowerCase()).toContain('description');
|
||||
expect(output.toLowerCase()).toContain('required');
|
||||
});
|
||||
|
||||
it('should display validation error for first invalid task when multiple tasks exist', () => {
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Valid Task',
|
||||
description: 'This one is fine',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Invalid Task',
|
||||
description: '', // ❌ Invalid - this should trigger error
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Another Valid Task',
|
||||
description: 'This would be fine too',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: 3,
|
||||
completedCount: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(output.toLowerCase()).toContain('description');
|
||||
expect(output.toLowerCase()).toContain('required');
|
||||
});
|
||||
|
||||
it('should handle all valid tasks without errors', () => {
|
||||
// This test verifies the fix doesn't break valid scenarios
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Valid Task 1',
|
||||
description: 'This task is valid',
|
||||
status: 'pending',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Valid Task 2',
|
||||
description: 'This task is also valid',
|
||||
status: 'done',
|
||||
priority: 'medium'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runList();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Valid Task 1');
|
||||
expect(output).toContain('Valid Task 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
290
apps/cli/tests/integration/commands/next.command.test.ts
Normal file
290
apps/cli/tests/integration/commands/next.command.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for 'task-master next' command
|
||||
*
|
||||
* Tests the next command which finds the next available task based on dependencies.
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
|
||||
import { getCliBinPath } from '../../helpers/test-utils';
|
||||
|
||||
// Capture initial working directory at module load time
|
||||
const initialCwd = process.cwd();
|
||||
|
||||
describe('next command', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let binPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-next-test-'));
|
||||
process.chdir(testDir);
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
binPath = getCliBinPath();
|
||||
|
||||
execSync(`node "${binPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Use fixture to create initial empty tasks file
|
||||
const initialTasks = createTasksFile();
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
// Restore to the original working directory captured at module load
|
||||
process.chdir(initialCwd);
|
||||
} catch {
|
||||
// Fallback to home directory if initial directory no longer exists
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
const runNext = (): { output: string; exitCode: number } => {
|
||||
try {
|
||||
const output = execSync(`node "${binPath}" next`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
return { output, exitCode: 0 };
|
||||
} catch (error: any) {
|
||||
// For errors, prioritize stderr (where error messages go)
|
||||
return {
|
||||
output: error.stderr?.toString() || error.stdout?.toString() || '',
|
||||
exitCode: error.status || 1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
it('should find first pending task with no dependencies', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'First Available Task',
|
||||
description: 'No dependencies',
|
||||
status: 'pending'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('First Available Task');
|
||||
});
|
||||
|
||||
it('should return task when dependencies are completed', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Prerequisite', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Ready Task',
|
||||
description: 'Dependencies met',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Ready Task');
|
||||
});
|
||||
|
||||
it('should skip tasks with incomplete dependencies', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Foundation Task', status: 'pending' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Blocked Task',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({ id: 3, title: 'Independent Task', status: 'pending' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
// Should return either task 1 or task 3 (both have no dependencies)
|
||||
const hasFoundation = output.includes('Foundation Task');
|
||||
const hasIndependent = output.includes('Independent Task');
|
||||
expect(hasFoundation || hasIndependent).toBe(true);
|
||||
expect(output).not.toContain('Blocked Task');
|
||||
});
|
||||
|
||||
it('should handle complex dependency chain', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Level 1', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Level 2',
|
||||
status: 'done',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Level 3 - Next',
|
||||
description: 'Should be next',
|
||||
status: 'pending',
|
||||
dependencies: ['1', '2']
|
||||
}),
|
||||
createTask({
|
||||
id: 4,
|
||||
title: 'Level 3 - Blocked',
|
||||
status: 'pending',
|
||||
dependencies: ['3']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Level 3 - Next');
|
||||
expect(output).not.toContain('Blocked');
|
||||
});
|
||||
|
||||
it('should skip already completed tasks', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Already Done', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Also Done', status: 'done' }),
|
||||
createTask({ id: 3, title: 'Next Up', status: 'pending' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Next Up');
|
||||
expect(output).not.toContain('Already Done');
|
||||
expect(output).not.toContain('Also Done');
|
||||
});
|
||||
|
||||
it('should handle empty task list', () => {
|
||||
const testData = createTasksFile();
|
||||
writeTasks(testData);
|
||||
|
||||
const { output } = runNext();
|
||||
|
||||
expect(output.toLowerCase()).toContain('no');
|
||||
});
|
||||
|
||||
it('should handle all tasks completed', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done 1', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Done 2', status: 'done' }),
|
||||
createTask({ id: 3, title: 'Done 3', status: 'done' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output } = runNext();
|
||||
|
||||
expect(output.toLowerCase()).toContain('no');
|
||||
});
|
||||
|
||||
it('should find first task in linear dependency chain', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Step 1', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Step 2',
|
||||
status: 'done',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Step 3',
|
||||
status: 'pending',
|
||||
dependencies: ['2']
|
||||
}),
|
||||
createTask({
|
||||
id: 4,
|
||||
title: 'Step 4',
|
||||
status: 'pending',
|
||||
dependencies: ['3']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Step 3');
|
||||
expect(output).not.toContain('Step 4');
|
||||
});
|
||||
|
||||
it('should find task among multiple ready tasks', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Foundation', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Ready Task A',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Ready Task B',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 4,
|
||||
title: 'Ready Task C',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runNext();
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
// Should return one of the ready tasks
|
||||
const hasReadyA = output.includes('Ready Task A');
|
||||
const hasReadyB = output.includes('Ready Task B');
|
||||
const hasReadyC = output.includes('Ready Task C');
|
||||
expect(hasReadyA || hasReadyB || hasReadyC).toBe(true);
|
||||
});
|
||||
});
|
||||
208
apps/cli/tests/integration/commands/set-status.command.test.ts
Normal file
208
apps/cli/tests/integration/commands/set-status.command.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for 'task-master set-status' command
|
||||
*
|
||||
* Tests the set-status command which updates task status.
|
||||
* Extracted from task-lifecycle.test.ts for better organization.
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
|
||||
import { getCliBinPath } from '../../helpers/test-utils';
|
||||
|
||||
// Capture initial working directory at module load time
|
||||
const initialCwd = process.cwd();
|
||||
|
||||
describe('set-status command', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let binPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-status-test-'));
|
||||
process.chdir(testDir);
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
binPath = getCliBinPath();
|
||||
|
||||
execSync(`node "${binPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Use fixture to create initial empty tasks file
|
||||
const initialTasks = createTasksFile();
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
// Restore to the original working directory captured at module load
|
||||
process.chdir(initialCwd);
|
||||
} catch {
|
||||
// Fallback to home directory if initial directory no longer exists
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
const readTasks = () => {
|
||||
const content = fs.readFileSync(tasksPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
};
|
||||
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
const runSetStatus = (id: number, status: string) => {
|
||||
return execSync(
|
||||
`node "${binPath}" set-status --id=${id} --status=${status}`,
|
||||
{
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
it('should update task status from pending to done', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Task to Complete',
|
||||
description: 'A task we will mark as done',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
details: 'Implementation details',
|
||||
testStrategy: 'Test strategy'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
runSetStatus(1, 'done');
|
||||
|
||||
const tasks = readTasks();
|
||||
const updatedTask = tasks.master.tasks.find((t: any) => t.id == 1);
|
||||
expect(updatedTask).toBeDefined();
|
||||
expect(updatedTask.status).toBe('done');
|
||||
expect(updatedTask.title).toBe('Task to Complete');
|
||||
});
|
||||
|
||||
it('should handle multiple status changes in sequence', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [createTask({ id: 1, title: 'Task', status: 'pending' })]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
runSetStatus(1, 'in-progress');
|
||||
let tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('in-progress');
|
||||
|
||||
runSetStatus(1, 'review');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('review');
|
||||
|
||||
runSetStatus(1, 'done');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('done');
|
||||
});
|
||||
|
||||
it('should reject invalid status values', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [createTask({ id: 1, title: 'Task', status: 'pending' })]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
expect(() => {
|
||||
runSetStatus(1, 'invalid');
|
||||
}).toThrow();
|
||||
|
||||
const tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should update status to all valid values', () => {
|
||||
const validStatuses = [
|
||||
'pending',
|
||||
'in-progress',
|
||||
'done',
|
||||
'review',
|
||||
'deferred',
|
||||
'cancelled'
|
||||
];
|
||||
|
||||
for (const status of validStatuses) {
|
||||
const testData = createTasksFile({
|
||||
tasks: [createTask({ id: 1, title: 'Test', status: 'pending' })]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
runSetStatus(1, status);
|
||||
|
||||
const tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe(status);
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve other task fields when updating status', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Preserve Fields',
|
||||
description: 'Original description',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: ['2'],
|
||||
details: 'Original details',
|
||||
testStrategy: 'Original strategy'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
runSetStatus(1, 'done');
|
||||
|
||||
const tasks = readTasks();
|
||||
const task = tasks.master.tasks[0];
|
||||
expect(task.status).toBe('done');
|
||||
expect(task.title).toBe('Preserve Fields');
|
||||
expect(task.description).toBe('Original description');
|
||||
expect(task.priority).toBe('high');
|
||||
expect(task.dependencies).toEqual(['2']);
|
||||
expect(task.details).toBe('Original details');
|
||||
expect(task.testStrategy).toBe('Original strategy');
|
||||
});
|
||||
|
||||
it('should handle multiple tasks correctly', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Task 1', status: 'pending' }),
|
||||
createTask({ id: 2, title: 'Task 2', status: 'pending' }),
|
||||
createTask({ id: 3, title: 'Task 3', status: 'pending' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
runSetStatus(2, 'done');
|
||||
|
||||
const tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('pending'); // Task 1 unchanged
|
||||
expect(tasks.master.tasks[1].status).toBe('done'); // Task 2 updated
|
||||
expect(tasks.master.tasks[2].status).toBe('pending'); // Task 3 unchanged
|
||||
});
|
||||
});
|
||||
260
apps/cli/tests/integration/commands/show.command.test.ts
Normal file
260
apps/cli/tests/integration/commands/show.command.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for 'task-master show' command
|
||||
*
|
||||
* Tests the show command which displays detailed information about a specific task.
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createTask,
|
||||
createTasksFile,
|
||||
createSubtask
|
||||
} from '../../fixtures/task-fixtures';
|
||||
import { getCliBinPath } from '../../helpers/test-utils';
|
||||
|
||||
// Capture initial working directory at module load time
|
||||
const initialCwd = process.cwd();
|
||||
|
||||
describe('show command', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let binPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-show-test-'));
|
||||
process.chdir(testDir);
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
binPath = getCliBinPath();
|
||||
|
||||
execSync(`node "${binPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Use fixture to create initial empty tasks file
|
||||
const initialTasks = createTasksFile();
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
// Restore to the original working directory captured at module load
|
||||
process.chdir(initialCwd);
|
||||
} catch {
|
||||
// Fallback to home directory if initial directory no longer exists
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
const runShow = (taskId: string): { output: string; exitCode: number } => {
|
||||
try {
|
||||
const output = execSync(`node "${binPath}" show ${taskId}`, {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
return { output, exitCode: 0 };
|
||||
} catch (error: any) {
|
||||
// For errors, prioritize stderr (where error messages go)
|
||||
return {
|
||||
output: error.stderr?.toString() || error.stdout?.toString() || '',
|
||||
exitCode: error.status || 1
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
it('should display complete task details', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Detailed Task',
|
||||
description: 'A comprehensive task description',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
details: 'Implementation details go here',
|
||||
testStrategy: 'Unit tests and integration tests'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runShow('1');
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Detailed Task');
|
||||
expect(output).toContain('A comprehensive task description');
|
||||
expect(output).toContain('pending');
|
||||
expect(output).toContain('high');
|
||||
expect(output).toContain('Implementation details');
|
||||
expect(output).toContain('Unit tests and integration tests');
|
||||
});
|
||||
|
||||
it('should show task with dependencies', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'First Task', status: 'done' }),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Second Task',
|
||||
description: 'Depends on task 1',
|
||||
status: 'pending',
|
||||
dependencies: ['1']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runShow('2');
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Second Task');
|
||||
expect(output).toContain('Depends on task 1');
|
||||
});
|
||||
|
||||
it('should show task with subtasks', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
description: 'Task with multiple subtasks',
|
||||
status: 'in-progress',
|
||||
subtasks: [
|
||||
createSubtask({
|
||||
id: '1.1',
|
||||
title: 'Setup Phase',
|
||||
description: 'Initial setup',
|
||||
status: 'done',
|
||||
parentId: '1'
|
||||
}),
|
||||
createSubtask({
|
||||
id: '1.2',
|
||||
title: 'Implementation Phase',
|
||||
description: 'Build feature',
|
||||
status: 'in-progress',
|
||||
dependencies: ['1.1'],
|
||||
parentId: '1'
|
||||
}),
|
||||
createSubtask({
|
||||
id: '1.3',
|
||||
title: 'Testing Phase',
|
||||
description: 'Write tests',
|
||||
status: 'pending',
|
||||
dependencies: ['1.2'],
|
||||
parentId: '1'
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runShow('1');
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Parent Task');
|
||||
expect(output).toContain('Setup Phase');
|
||||
expect(output).toContain('Implementation Phase');
|
||||
expect(output).toContain('Testing Phase');
|
||||
});
|
||||
|
||||
it('should show minimal task information', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [createTask({ id: 1, title: 'Simple Task', status: 'pending' })]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const { output, exitCode } = runShow('1');
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain('Simple Task');
|
||||
expect(output).toContain('pending');
|
||||
});
|
||||
|
||||
it('should show task with all status types', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done Task', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Pending Task', status: 'pending' }),
|
||||
createTask({ id: 3, title: 'In Progress', status: 'in-progress' }),
|
||||
createTask({ id: 4, title: 'Review Task', status: 'review' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
// Test each status
|
||||
let result = runShow('1');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain('Done Task');
|
||||
|
||||
result = runShow('2');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain('Pending Task');
|
||||
|
||||
result = runShow('3');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain('In Progress');
|
||||
|
||||
result = runShow('4');
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain('Review Task');
|
||||
});
|
||||
|
||||
it('should show task with priority levels', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'High Priority',
|
||||
status: 'pending',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Medium Priority',
|
||||
status: 'pending',
|
||||
priority: 'medium'
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Low Priority',
|
||||
status: 'pending',
|
||||
priority: 'low'
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
let result = runShow('1');
|
||||
expect(result.output).toContain('High Priority');
|
||||
expect(result.output).toContain('high');
|
||||
|
||||
result = runShow('2');
|
||||
expect(result.output).toContain('Medium Priority');
|
||||
expect(result.output).toContain('medium');
|
||||
|
||||
result = runShow('3');
|
||||
expect(result.output).toContain('Low Priority');
|
||||
expect(result.output).toContain('low');
|
||||
});
|
||||
});
|
||||
414
apps/cli/tests/integration/task-lifecycle.test.ts
Normal file
414
apps/cli/tests/integration/task-lifecycle.test.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for basic task lifecycle operations
|
||||
*
|
||||
* TESTING PHILOSOPHY:
|
||||
* - These are TRUE integration tests - we spawn real CLI processes
|
||||
* - We use real file system operations (temp directories)
|
||||
* - We verify behavior by checking file system changes
|
||||
* - We avoid mocking except for AI SDK to save costs
|
||||
*
|
||||
* WHY TEST FILE CHANGES INSTEAD OF CLI OUTPUT:
|
||||
* - CLI output is formatted for humans (colors, boxes, tables)
|
||||
* - File system changes are the source of truth
|
||||
* - More stable - UI can change, but data format is stable
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getCliBinPath } from '../helpers/test-utils';
|
||||
|
||||
// Capture initial working directory at module load time
|
||||
const initialCwd = process.cwd();
|
||||
|
||||
describe('Task Lifecycle Integration Tests', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let binPath: string;
|
||||
|
||||
/**
|
||||
* SETUP PATTERN:
|
||||
* Before each test, we:
|
||||
* 1. Create an isolated temp directory (no cross-test pollution)
|
||||
* 2. Change into it (CLI commands run in this context)
|
||||
* 3. Initialize a fresh Task Master project
|
||||
* 4. Skip auto-updates for deterministic timing
|
||||
*/
|
||||
beforeEach(() => {
|
||||
// Create isolated test environment in OS temp directory
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-test-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
// Disable auto-update checks for deterministic test timing
|
||||
process.env.TASKMASTER_SKIP_AUTO_UPDATE = '1';
|
||||
|
||||
// Path to the compiled CLI binary we're testing
|
||||
// Binary is built to root dist/ directory, not apps/cli/dist/
|
||||
binPath = getCliBinPath();
|
||||
|
||||
// Initialize a fresh Task Master project
|
||||
execSync(`node "${binPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
TASKMASTER_SKIP_AUTO_UPDATE: '1'
|
||||
}
|
||||
});
|
||||
|
||||
// Path where tasks.json will be stored
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Create initial tasks.json (init doesn't create it until first task added)
|
||||
const initialTasks = {
|
||||
master: {
|
||||
tasks: [],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
description: 'Test tasks'
|
||||
}
|
||||
}
|
||||
};
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
/**
|
||||
* CLEANUP PATTERN:
|
||||
* After each test:
|
||||
* 1. Change back to original directory (can't delete current dir)
|
||||
* 2. Delete the temp directory recursively
|
||||
* 3. Clean up environment variables
|
||||
*
|
||||
* WHY: Prevents "directory in use" errors and disk space leaks
|
||||
*/
|
||||
afterEach(() => {
|
||||
try {
|
||||
// Restore to the original working directory captured at module load
|
||||
process.chdir(initialCwd);
|
||||
} catch (error) {
|
||||
// Fallback to home directory if initial directory no longer exists
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
// Remove test directory and all contents
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Clean up environment
|
||||
delete process.env.TASKMASTER_SKIP_AUTO_UPDATE;
|
||||
});
|
||||
|
||||
/**
|
||||
* TEST HELPER: Read tasks from tasks.json
|
||||
*
|
||||
* EDUCATIONAL NOTE:
|
||||
* We read the actual file from disk, not mocked data.
|
||||
* This validates that the CLI actually wrote what we expect.
|
||||
*/
|
||||
const readTasks = () => {
|
||||
const content = fs.readFileSync(tasksPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* TEST HELPER: Write tasks to tasks.json
|
||||
*
|
||||
* EDUCATIONAL NOTE:
|
||||
* We manually create test data by writing to the real file system.
|
||||
* This simulates different project states without AI calls.
|
||||
*/
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
/**
|
||||
* TEST HELPER: Run a CLI command with auto-update disabled
|
||||
*
|
||||
* EDUCATIONAL NOTE:
|
||||
* This helper ensures TASKMASTER_SKIP_AUTO_UPDATE is always set,
|
||||
* avoiding repetition and ensuring consistent test behavior.
|
||||
*/
|
||||
const runCommand = (command: string, options: any = {}) => {
|
||||
return execSync(`node "${binPath}" ${command}`, {
|
||||
...options,
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
};
|
||||
|
||||
describe('task-master init', () => {
|
||||
it('should initialize project structure', () => {
|
||||
// ASSERTION PATTERN:
|
||||
// We verify the actual directory structure was created
|
||||
expect(fs.existsSync(path.join(testDir, '.taskmaster'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'tasks'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'docs'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(fs.existsSync(path.join(testDir, '.taskmaster', 'reports'))).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
fs.existsSync(path.join(testDir, '.taskmaster', 'config.json'))
|
||||
).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(testDir, '.taskmaster', 'state.json'))
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task-master set-status', () => {
|
||||
it('should update task status from pending to done', () => {
|
||||
// ARRANGE: Create a pending task
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task to Complete',
|
||||
description: 'A task we will mark as done',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Implementation details',
|
||||
testStrategy: 'Test strategy',
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
// ACT: Mark task as done via CLI
|
||||
runCommand('set-status --id=1 --status=done', { stdio: 'pipe' });
|
||||
|
||||
// ASSERT: Verify status was updated in actual file
|
||||
const tasks = readTasks();
|
||||
// Note: CLI may convert id from number to string
|
||||
const updatedTask = tasks.master.tasks.find((t: any) => t.id == 1); // == handles both number and string
|
||||
expect(updatedTask).toBeDefined();
|
||||
expect(updatedTask.status).toBe('done');
|
||||
expect(updatedTask.title).toBe('Task to Complete'); // Other fields unchanged
|
||||
});
|
||||
|
||||
it('should update subtask status', () => {
|
||||
// ARRANGE: Create task with subtasks
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
description: 'Parent task description',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Parent task details',
|
||||
testStrategy: 'Test strategy',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'First Subtask',
|
||||
description: 'Subtask to complete',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: 'Subtask details'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Second Subtask',
|
||||
description: 'Second subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: ['1.1'],
|
||||
details: 'Second subtask details'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
description: 'Test tasks'
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
// ACT: Mark subtask as done
|
||||
runCommand('set-status --id=1.1 --status=done', { stdio: 'pipe' });
|
||||
|
||||
// ASSERT: Verify subtask status updated
|
||||
const tasks = readTasks();
|
||||
const parentTask = tasks.master.tasks.find((t: any) => t.id == 1);
|
||||
expect(parentTask).toBeDefined();
|
||||
const subtask = parentTask.subtasks.find((s: any) => s.id == 1);
|
||||
expect(subtask).toBeDefined();
|
||||
expect(subtask.status).toBe('done');
|
||||
|
||||
// Verify other subtask unchanged
|
||||
const otherSubtask = parentTask.subtasks.find((s: any) => s.id == 2);
|
||||
expect(otherSubtask.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('should handle multiple status changes in sequence', () => {
|
||||
// ARRANGE: Create task
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
// ACT & ASSERT: Change status multiple times
|
||||
runCommand('set-status --id=1 --status=in-progress', { stdio: 'pipe' });
|
||||
let tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('in-progress');
|
||||
|
||||
runCommand('set-status --id=1 --status=review', { stdio: 'pipe' });
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('review');
|
||||
|
||||
runCommand('set-status --id=1 --status=done', { stdio: 'pipe' });
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('done');
|
||||
});
|
||||
|
||||
it('should reject invalid status values', () => {
|
||||
// ARRANGE: Create task
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
// ACT & ASSERT: Should throw on invalid status
|
||||
expect(() => {
|
||||
runCommand('set-status --id=1 --status=invalid', { stdio: 'pipe' });
|
||||
}).toThrow();
|
||||
|
||||
// Verify status unchanged
|
||||
const tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* EDUCATIONAL NOTE: Real-World Workflow Test
|
||||
*
|
||||
* This test demonstrates a realistic workflow:
|
||||
* 1. Start with pending tasks
|
||||
* 2. Mark them as in-progress
|
||||
* 3. Complete them one by one
|
||||
* 4. Verify final state
|
||||
*
|
||||
* This is the kind of flow a real developer would follow.
|
||||
*/
|
||||
describe('Realistic Task Workflow', () => {
|
||||
it('should support typical development workflow', () => {
|
||||
// ARRANGE: Create realistic project tasks
|
||||
const testData = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Environment',
|
||||
description: 'Install dependencies and configure',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Write Tests',
|
||||
description: 'Create test suite',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [1],
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Implement Feature',
|
||||
description: 'Write actual code',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [2],
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
description: 'Sample project'
|
||||
}
|
||||
}
|
||||
};
|
||||
writeTasks(testData);
|
||||
|
||||
// ACT & ASSERT: Work through tasks in realistic order
|
||||
|
||||
// Developer starts task 1
|
||||
runCommand('set-status --id=1 --status=in-progress');
|
||||
let tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('in-progress');
|
||||
|
||||
// Developer completes task 1
|
||||
runCommand('set-status --id=1 --status=done');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[0].status).toBe('done');
|
||||
|
||||
// Developer starts task 2
|
||||
runCommand('set-status --id=2 --status=in-progress');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[1].status).toBe('in-progress');
|
||||
|
||||
// Developer completes task 2
|
||||
runCommand('set-status --id=2 --status=done');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[1].status).toBe('done');
|
||||
|
||||
// Developer starts and completes task 3
|
||||
runCommand('set-status --id=3 --status=in-progress');
|
||||
runCommand('set-status --id=3 --status=done');
|
||||
tasks = readTasks();
|
||||
expect(tasks.master.tasks[2].status).toBe('done');
|
||||
|
||||
// Verify final state: all tasks done
|
||||
expect(tasks.master.tasks.every((t: any) => t.status === 'done')).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,21 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import rootConfig from '../../vitest.config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'tests/',
|
||||
'**/*.test.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.d.ts',
|
||||
'**/mocks/**',
|
||||
'**/fixtures/**',
|
||||
'vitest.config.ts'
|
||||
/**
|
||||
* CLI package Vitest configuration
|
||||
* Extends root config with CLI-specific settings
|
||||
*/
|
||||
export default mergeConfig(
|
||||
rootConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
// CLI-specific test patterns
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -23,14 +23,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"zod": "^4.1.11",
|
||||
"fastmcp": "^3.23.0"
|
||||
"fastmcp": "^3.23.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
|
||||
export * from './tools/autopilot/index.js';
|
||||
export * from './tools/tasks/index.js';
|
||||
// TODO: Re-enable when TypeScript dependency tools are implemented
|
||||
// export * from './tools/dependencies/index.js';
|
||||
export * from './shared/utils.js';
|
||||
export * from './shared/types.js';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Shared types for MCP tools
|
||||
*/
|
||||
|
||||
import type { TmCore } from '@tm/core';
|
||||
|
||||
export interface MCPResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
@@ -31,6 +33,29 @@ export interface MCPContext {
|
||||
session: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced MCP context with tmCore instance
|
||||
*/
|
||||
export interface ToolContext {
|
||||
/** Logger instance (matches fastmcp's Context.log signature) */
|
||||
log: {
|
||||
info: (message: string, data?: any) => void;
|
||||
warn: (message: string, data?: any) => void;
|
||||
error: (message: string, data?: any) => void;
|
||||
debug: (message: string, data?: any) => void;
|
||||
};
|
||||
/** MCP session */
|
||||
session?: {
|
||||
roots?: Array<{ uri: string; name?: string }>;
|
||||
env?: Record<string, string>;
|
||||
clientCapabilities?: {
|
||||
sampling?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
/** TmCore instance (already initialized) */
|
||||
tmCore: TmCore;
|
||||
}
|
||||
|
||||
export interface WithProjectRoot {
|
||||
projectRoot: string;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
* Shared utilities for MCP tools
|
||||
*/
|
||||
|
||||
import type { ContentResult } from 'fastmcp';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
LOCAL_ONLY_COMMANDS,
|
||||
createTmCore,
|
||||
type LocalOnlyCommand
|
||||
} from '@tm/core';
|
||||
import type { ContentResult, Context } from 'fastmcp';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
import type { ToolContext } from './types.js';
|
||||
|
||||
/**
|
||||
* Get version information
|
||||
@@ -17,6 +23,82 @@ export function getVersionInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a content response for MCP tools
|
||||
* FastMCP requires text type, so we format objects as JSON strings
|
||||
*/
|
||||
export function createContentResponse(content: any): ContentResult {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
typeof content === 'object'
|
||||
? // Format JSON nicely with indentation
|
||||
JSON.stringify(content, null, 2)
|
||||
: // Keep other content types as-is
|
||||
String(content)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error response for MCP tools
|
||||
*/
|
||||
export function createErrorResponse(
|
||||
errorMessage: string,
|
||||
versionInfo?: { version: string; name: string },
|
||||
tagInfo?: { currentTag: string }
|
||||
): ContentResult {
|
||||
// Provide fallback version info if not provided
|
||||
if (!versionInfo) {
|
||||
versionInfo = getVersionInfo();
|
||||
}
|
||||
|
||||
let responseText = `Error: ${errorMessage}
|
||||
Version: ${versionInfo.version}
|
||||
Name: ${versionInfo.name}`;
|
||||
|
||||
// Add tag information if available
|
||||
if (tagInfo) {
|
||||
responseText += `
|
||||
Current Tag: ${tagInfo.currentTag}`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: responseText
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function signature for progress reporting
|
||||
*/
|
||||
export type ReportProgressFn = (progress: number, total?: number) => void;
|
||||
|
||||
/**
|
||||
* Validate that reportProgress is available for long-running operations
|
||||
*/
|
||||
export function checkProgressCapability(
|
||||
reportProgress: any,
|
||||
log: any
|
||||
): ReportProgressFn | undefined {
|
||||
if (typeof reportProgress !== 'function') {
|
||||
log?.debug?.(
|
||||
'reportProgress not available - operation will run without progress updates'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return reportProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tag for a project root
|
||||
*/
|
||||
@@ -183,7 +265,7 @@ function getProjectRootFromSession(session: any): string | null {
|
||||
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
|
||||
fn: (
|
||||
args: T & { projectRoot: string },
|
||||
context: any
|
||||
context: Context<undefined>
|
||||
) => Promise<ContentResult>
|
||||
): (args: T, context: any) => Promise<ContentResult> {
|
||||
return async (args: T, context: any): Promise<ContentResult> => {
|
||||
@@ -268,3 +350,87 @@ export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool execution function signature with tmCore provided
|
||||
*/
|
||||
export type ToolExecuteFn<TArgs = any, TResult = any> = (
|
||||
args: TArgs,
|
||||
context: ToolContext
|
||||
) => Promise<TResult>;
|
||||
|
||||
/**
|
||||
* Higher-order function that wraps MCP tool execution with:
|
||||
* - Normalized project root (via withNormalizedProjectRoot)
|
||||
* - TmCore instance creation
|
||||
* - Command guard check (for local-only commands)
|
||||
*
|
||||
* Use this for ALL MCP tools to provide consistent context and auth checking.
|
||||
*
|
||||
* @param commandName - Name of the command (used for guard check)
|
||||
* @param executeFn - Tool execution function that receives args and enhanced context
|
||||
* @returns Wrapped execute function
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export function registerAddDependencyTool(server: FastMCP) {
|
||||
* server.addTool({
|
||||
* name: 'add_dependency',
|
||||
* parameters: AddDependencySchema,
|
||||
* execute: withToolContext('add-dependency', async (args, context) => {
|
||||
* // context.tmCore is already available
|
||||
* // Auth guard already checked
|
||||
* // Just implement the tool logic!
|
||||
* })
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function withToolContext<TArgs extends { projectRoot?: string }>(
|
||||
commandName: string,
|
||||
executeFn: ToolExecuteFn<TArgs & { projectRoot: string }, ContentResult>
|
||||
) {
|
||||
return withNormalizedProjectRoot(
|
||||
async (
|
||||
args: TArgs & { projectRoot: string },
|
||||
context: Context<undefined>
|
||||
) => {
|
||||
// Create tmCore instance
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: args.projectRoot,
|
||||
loggerConfig: { mcpMode: true, logCallback: context.log }
|
||||
});
|
||||
|
||||
// Check if this is a local-only command that needs auth guard
|
||||
if (LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand)) {
|
||||
const authResult = await tmCore.auth.guardCommand(
|
||||
commandName,
|
||||
tmCore.tasks.getStorageType()
|
||||
);
|
||||
|
||||
if (authResult.isBlocked) {
|
||||
const errorMsg = `You're working on the ${authResult.briefName} Brief in Hamster so this command is managed for you. This command is only available for local file storage. Log out with 'tm auth logout' to use local commands.`;
|
||||
context.log.info(errorMsg);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: errorMsg }
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create enhanced context with tmCore
|
||||
const enhancedContext: ToolContext = {
|
||||
log: context.log,
|
||||
session: context.session,
|
||||
tmCore
|
||||
};
|
||||
|
||||
// Execute the actual tool logic with enhanced context
|
||||
return executeFn(args, enhancedContext);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
* Abort a running TDD workflow and clean up state
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
import { z } from 'zod';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
|
||||
const AbortSchema = z.object({
|
||||
projectRoot: z
|
||||
@@ -29,12 +26,13 @@ export function registerAutopilotAbortTool(server: FastMCP) {
|
||||
description:
|
||||
'Abort the current TDD workflow and clean up workflow state. This will remove the workflow state file but will NOT delete the git branch or any code changes.',
|
||||
parameters: AbortSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: AbortArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-abort',
|
||||
async (args: AbortArgs, { log }: ToolContext) => {
|
||||
const { projectRoot } = args;
|
||||
|
||||
try {
|
||||
context.log.info(`Aborting autopilot workflow in ${projectRoot}`);
|
||||
log.info(`Aborting autopilot workflow in ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -42,7 +40,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
|
||||
const hasWorkflow = await workflowService.hasWorkflow();
|
||||
|
||||
if (!hasWorkflow) {
|
||||
context.log.warn('No active workflow to abort');
|
||||
log.warn('No active workflow to abort');
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
@@ -51,7 +49,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
|
||||
hadWorkflow: false
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -63,7 +61,7 @@ export function registerAutopilotAbortTool(server: FastMCP) {
|
||||
// Abort workflow
|
||||
await workflowService.abortWorkflow();
|
||||
|
||||
context.log.info('Workflow state deleted');
|
||||
log.info('Workflow state deleted');
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -76,20 +74,20 @@ export function registerAutopilotAbortTool(server: FastMCP) {
|
||||
note: 'Git branch and code changes were preserved. You can manually clean them up if needed.'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-abort: ${error.message}`);
|
||||
log.error(`Error in autopilot-abort: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to abort workflow: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
* Create a git commit with automatic staging and message generation
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { WorkflowService, GitAdapter, CommitMessageGenerator } from '@tm/core';
|
||||
import { CommitMessageGenerator, GitAdapter, WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
import { z } from 'zod';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
|
||||
const CommitSchema = z.object({
|
||||
projectRoot: z
|
||||
@@ -39,12 +36,13 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
description:
|
||||
'Create a git commit with automatic staging, message generation, and metadata embedding. Generates appropriate commit messages based on subtask context and TDD phase.',
|
||||
parameters: CommitSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: CommitArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-commit',
|
||||
async (args: CommitArgs, { log }: ToolContext) => {
|
||||
const { projectRoot, files, customMessage } = args;
|
||||
|
||||
try {
|
||||
context.log.info(`Creating commit for workflow in ${projectRoot}`);
|
||||
log.info(`Creating commit for workflow in ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -58,7 +56,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
'No active workflow found. Start a workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -70,9 +68,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
|
||||
// Verify we're in COMMIT phase
|
||||
if (status.tddPhase !== 'COMMIT') {
|
||||
context.log.warn(
|
||||
`Not in COMMIT phase (currently in ${status.tddPhase})`
|
||||
);
|
||||
log.warn(`Not in COMMIT phase (currently in ${status.tddPhase})`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
@@ -80,7 +76,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
message: `Cannot commit: currently in ${status.tddPhase} phase. Complete the ${status.tddPhase} phase first using autopilot_complete_phase`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -92,7 +88,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
success: false,
|
||||
error: { message: 'No active subtask to commit' }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -104,19 +100,19 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
try {
|
||||
if (files && files.length > 0) {
|
||||
await gitAdapter.stageFiles(files);
|
||||
context.log.info(`Staged ${files.length} files`);
|
||||
log.info(`Staged ${files.length} files`);
|
||||
} else {
|
||||
await gitAdapter.stageFiles(['.']);
|
||||
context.log.info('Staged all changes');
|
||||
log.info('Staged all changes');
|
||||
}
|
||||
} catch (error: any) {
|
||||
context.log.error(`Failed to stage files: ${error.message}`);
|
||||
log.error(`Failed to stage files: ${error.message}`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to stage files: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -124,7 +120,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
// Check if there are staged changes
|
||||
const hasStagedChanges = await gitAdapter.hasStagedChanges();
|
||||
if (!hasStagedChanges) {
|
||||
context.log.warn('No staged changes to commit');
|
||||
log.warn('No staged changes to commit');
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
@@ -133,7 +129,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
'No staged changes to commit. Make code changes before committing'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -145,7 +141,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
let commitMessage: string;
|
||||
if (customMessage) {
|
||||
commitMessage = customMessage;
|
||||
context.log.info('Using custom commit message');
|
||||
log.info('Using custom commit message');
|
||||
} else {
|
||||
const messageGenerator = new CommitMessageGenerator();
|
||||
|
||||
@@ -168,21 +164,21 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
};
|
||||
|
||||
commitMessage = messageGenerator.generateMessage(options);
|
||||
context.log.info('Generated commit message automatically');
|
||||
log.info('Generated commit message automatically');
|
||||
}
|
||||
|
||||
// Create commit
|
||||
try {
|
||||
await gitAdapter.createCommit(commitMessage);
|
||||
context.log.info('Commit created successfully');
|
||||
log.info('Commit created successfully');
|
||||
} catch (error: any) {
|
||||
context.log.error(`Failed to create commit: ${error.message}`);
|
||||
log.error(`Failed to create commit: ${error.message}`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to create commit: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -193,7 +189,7 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
// Complete COMMIT phase and advance workflow
|
||||
const newStatus = await workflowService.commit();
|
||||
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Commit completed. Current phase: ${newStatus.tddPhase || newStatus.phase}`
|
||||
);
|
||||
|
||||
@@ -217,20 +213,20 @@ export function registerAutopilotCommitTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-commit: ${error.message}`);
|
||||
log.error(`Error in autopilot-commit: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to commit: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
* Complete the current TDD phase with test result validation
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
import { z } from 'zod';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
|
||||
const CompletePhaseSchema = z.object({
|
||||
projectRoot: z
|
||||
@@ -35,16 +32,15 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
server.addTool({
|
||||
name: 'autopilot_complete_phase',
|
||||
description:
|
||||
'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.',
|
||||
'Complete the current TDD phase (RED or GREEN) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing. For COMMIT phase, use autopilot_commit instead.',
|
||||
parameters: CompletePhaseSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: CompletePhaseArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-complete-phase',
|
||||
async (args: CompletePhaseArgs, { log }: ToolContext) => {
|
||||
const { projectRoot, testResults } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Completing current phase in workflow for ${projectRoot}`
|
||||
);
|
||||
log.info(`Completing current phase in workflow for ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -58,7 +54,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
'No active workflow found. Start a workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -76,7 +72,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -91,7 +87,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -112,7 +108,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
const status = await workflowService.completePhase(fullTestResults);
|
||||
const nextAction = workflowService.getNextAction();
|
||||
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Phase completed. New phase: ${status.tddPhase || status.phase}`
|
||||
);
|
||||
|
||||
@@ -127,13 +123,13 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-complete: ${error.message}`);
|
||||
log.error(`Error in autopilot-complete: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -142,7 +138,7 @@ export function registerAutopilotCompleteTool(server: FastMCP) {
|
||||
message: `Failed to complete phase: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
* Finalize and complete the workflow with working tree validation
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
import { z } from 'zod';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
|
||||
const FinalizeSchema = z.object({
|
||||
projectRoot: z
|
||||
@@ -29,12 +26,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
description:
|
||||
'Finalize and complete the workflow. Validates that all changes are committed and working tree is clean before marking workflow as complete.',
|
||||
parameters: FinalizeSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: FinalizeArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-finalize',
|
||||
async (args: FinalizeArgs, { log }: ToolContext) => {
|
||||
const { projectRoot } = args;
|
||||
|
||||
try {
|
||||
context.log.info(`Finalizing workflow in ${projectRoot}`);
|
||||
log.info(`Finalizing workflow in ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -48,7 +46,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
'No active workflow found. Start a workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -66,7 +64,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
message: `Cannot finalize: workflow is in ${currentStatus.phase} phase. Complete all subtasks first.`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -74,7 +72,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
// Finalize workflow (validates clean working tree)
|
||||
const newStatus = await workflowService.finalizeWorkflow();
|
||||
|
||||
context.log.info('Workflow finalized successfully');
|
||||
log.info('Workflow finalized successfully');
|
||||
|
||||
// Get next action
|
||||
const nextAction = workflowService.getNextAction();
|
||||
@@ -89,13 +87,13 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-finalize: ${error.message}`);
|
||||
log.error(`Error in autopilot-finalize: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -104,7 +102,7 @@ export function registerAutopilotFinalizeTool(server: FastMCP) {
|
||||
message: `Failed to finalize workflow: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
* Get the next action to perform in the TDD workflow
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
import { z } from 'zod';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
|
||||
const NextActionSchema = z.object({
|
||||
projectRoot: z
|
||||
@@ -29,14 +26,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
|
||||
description:
|
||||
'Get the next action to perform in the TDD workflow. Returns detailed context about what needs to be done next, including the current phase, subtask, and expected actions.',
|
||||
parameters: NextActionSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: NextActionArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-next',
|
||||
async (args: NextActionArgs, { log }: ToolContext) => {
|
||||
const { projectRoot } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Getting next action for workflow in ${projectRoot}`
|
||||
);
|
||||
log.info(`Getting next action for workflow in ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -50,7 +46,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
|
||||
'No active workflow found. Start a workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -62,7 +58,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
|
||||
const nextAction = workflowService.getNextAction();
|
||||
const status = workflowService.getStatus();
|
||||
|
||||
context.log.info(`Next action determined: ${nextAction.action}`);
|
||||
log.info(`Next action determined: ${nextAction.action}`);
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -74,13 +70,13 @@ export function registerAutopilotNextTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-next: ${error.message}`);
|
||||
log.error(`Error in autopilot-next: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -89,7 +85,7 @@ export function registerAutopilotNextTool(server: FastMCP) {
|
||||
message: `Failed to get next action: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
@@ -29,12 +26,13 @@ export function registerAutopilotResumeTool(server: FastMCP) {
|
||||
description:
|
||||
'Resume a previously started TDD workflow from saved state. Restores the workflow state machine and continues from where it left off.',
|
||||
parameters: ResumeWorkflowSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: ResumeWorkflowArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-resume',
|
||||
async (args: ResumeWorkflowArgs, { log }: ToolContext) => {
|
||||
const { projectRoot } = args;
|
||||
|
||||
try {
|
||||
context.log.info(`Resuming autopilot workflow in ${projectRoot}`);
|
||||
log.info(`Resuming autopilot workflow in ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -48,7 +46,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
|
||||
'No workflow state found. Start a new workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -57,9 +55,7 @@ export function registerAutopilotResumeTool(server: FastMCP) {
|
||||
const status = await workflowService.resumeWorkflow();
|
||||
const nextAction = workflowService.getNextAction();
|
||||
|
||||
context.log.info(
|
||||
`Workflow resumed successfully for task ${status.taskId}`
|
||||
);
|
||||
log.info(`Workflow resumed successfully for task ${status.taskId}`);
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -72,20 +68,20 @@ export function registerAutopilotResumeTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-resume: ${error.message}`);
|
||||
log.error(`Error in autopilot-resume: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to resume workflow: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore } from '@tm/core';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
@@ -57,12 +53,13 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
description:
|
||||
'Initialize and start a new TDD workflow for a task. Creates a git branch and sets up the workflow state machine.',
|
||||
parameters: StartWorkflowSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: StartWorkflowArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-start',
|
||||
async (args: StartWorkflowArgs, { log, tmCore }: ToolContext) => {
|
||||
const { taskId, projectRoot, maxAttempts, force } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Starting autopilot workflow for task ${taskId} in ${projectRoot}`
|
||||
);
|
||||
|
||||
@@ -75,20 +72,15 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
message: `Task ID "${taskId}" is a subtask. Autopilot workflows can only be started for main tasks (e.g., "1", "2", "HAM-123"). Please provide the parent task ID instead.`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
// Load task data and get current tag
|
||||
const core = await createTmCore({
|
||||
projectPath: projectRoot
|
||||
});
|
||||
|
||||
// Get current tag from ConfigManager
|
||||
const currentTag = core.config.getActiveTag();
|
||||
const currentTag = tmCore.config.getActiveTag();
|
||||
|
||||
const taskResult = await core.tasks.get(taskId);
|
||||
const taskResult = await tmCore.tasks.get(taskId);
|
||||
|
||||
if (!taskResult || !taskResult.task) {
|
||||
return handleApiResult({
|
||||
@@ -96,7 +88,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
success: false,
|
||||
error: { message: `Task ${taskId} not found` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -112,7 +104,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
message: `Task ${taskId} has no subtasks. Please use expand_task (with id="${taskId}") to create subtasks first. For improved results, consider running analyze_complexity before expanding the task.`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -123,7 +115,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
// Check for existing workflow
|
||||
const hasWorkflow = await workflowService.hasWorkflow();
|
||||
if (hasWorkflow && !force) {
|
||||
context.log.warn('Workflow state already exists');
|
||||
log.warn('Workflow state already exists');
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
@@ -132,7 +124,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
'Workflow already in progress. Use force=true to override or resume the existing workflow. Suggestion: Use autopilot_resume to continue the existing workflow'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -152,7 +144,7 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
tag: currentTag // Pass current tag for branch naming
|
||||
});
|
||||
|
||||
context.log.info(`Workflow started successfully for task ${taskId}`);
|
||||
log.info(`Workflow started successfully for task ${taskId}`);
|
||||
|
||||
// Get next action with guidance from WorkflowService
|
||||
const nextAction = workflowService.getNextAction();
|
||||
@@ -172,20 +164,20 @@ export function registerAutopilotStartTool(server: FastMCP) {
|
||||
nextSteps: nextAction.nextSteps
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-start: ${error.message}`);
|
||||
log.error(`Error in autopilot-start: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: { message: `Failed to start workflow: ${error.message}` }
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { handleApiResult, withToolContext } from '../../shared/utils.js';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { WorkflowService } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
@@ -29,12 +26,13 @@ export function registerAutopilotStatusTool(server: FastMCP) {
|
||||
description:
|
||||
'Get comprehensive workflow status including current phase, progress, subtask details, and activity history.',
|
||||
parameters: StatusSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: StatusArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'autopilot-status',
|
||||
async (args: StatusArgs, { log }: ToolContext) => {
|
||||
const { projectRoot } = args;
|
||||
|
||||
try {
|
||||
context.log.info(`Getting workflow status for ${projectRoot}`);
|
||||
log.info(`Getting workflow status for ${projectRoot}`);
|
||||
|
||||
const workflowService = new WorkflowService(projectRoot);
|
||||
|
||||
@@ -48,7 +46,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
|
||||
'No active workflow found. Start a workflow with autopilot_start'
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
@@ -59,22 +57,20 @@ export function registerAutopilotStatusTool(server: FastMCP) {
|
||||
// Get status
|
||||
const status = workflowService.getStatus();
|
||||
|
||||
context.log.info(
|
||||
`Workflow status retrieved for task ${status.taskId}`
|
||||
);
|
||||
log.info(`Workflow status retrieved for task ${status.taskId}`);
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: status
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in autopilot-status: ${error.message}`);
|
||||
log.error(`Error in autopilot-status: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -83,7 +79,7 @@ export function registerAutopilotStatusTool(server: FastMCP) {
|
||||
message: `Failed to get workflow status: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
withToolContext
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, Subtask, type Task } from '@tm/core';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import { Subtask, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTaskSchema = z.object({
|
||||
@@ -40,24 +40,16 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
name: 'get_task',
|
||||
description: 'Get detailed information about a specific task',
|
||||
parameters: GetTaskSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTaskArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'get-task',
|
||||
async (args: GetTaskArgs, { log, tmCore }: ToolContext) => {
|
||||
const { id, status, projectRoot, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Handle comma-separated IDs - parallelize for better performance
|
||||
const taskIds = id.split(',').map((tid) => tid.trim());
|
||||
const results = await Promise.all(
|
||||
@@ -83,7 +75,7 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
context.log.warn(`No tasks found for ID(s): ${id}`);
|
||||
log.warn(`No tasks found for ID(s): ${id}`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
@@ -91,12 +83,12 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
message: `No tasks found for ID(s): ${id}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}`
|
||||
);
|
||||
|
||||
@@ -108,14 +100,14 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
success: true,
|
||||
data: responseData
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot,
|
||||
tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-task: ${error.message}`);
|
||||
log.error(`Error in get-task: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -124,7 +116,7 @@ export function registerGetTaskTool(server: FastMCP) {
|
||||
message: `Failed to get task: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
withToolContext
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type TaskStatus, type Task } from '@tm/core';
|
||||
import type { ToolContext } from '../../shared/types.js';
|
||||
import type { TaskStatus, Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTasksSchema = z.object({
|
||||
@@ -40,24 +40,16 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
description:
|
||||
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||||
parameters: GetTasksSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTasksArgs, context: MCPContext) => {
|
||||
execute: withToolContext(
|
||||
'get-tasks',
|
||||
async (args: GetTasksArgs, { log, tmCore }: ToolContext) => {
|
||||
const { projectRoot, status, withSubtasks, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Build filter
|
||||
const filter =
|
||||
status && status !== 'all'
|
||||
@@ -75,13 +67,14 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
includeSubtasks: withSubtasks
|
||||
});
|
||||
|
||||
context.log.info(
|
||||
log.info(
|
||||
`Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
|
||||
);
|
||||
|
||||
// Calculate stats using reduce for cleaner code
|
||||
const tasks = result.tasks ?? [];
|
||||
const totalTasks = result.total;
|
||||
const taskCounts = result.tasks.reduce(
|
||||
const taskCounts = tasks.reduce(
|
||||
(acc, task) => {
|
||||
acc[task.status] = (acc[task.status] || 0) + 1;
|
||||
return acc;
|
||||
@@ -93,7 +86,7 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
totalTasks > 0 ? ((taskCounts.done || 0) / totalTasks) * 100 : 0;
|
||||
|
||||
// Count subtasks using reduce
|
||||
const subtaskCounts = result.tasks.reduce(
|
||||
const subtaskCounts = tasks.reduce(
|
||||
(acc, task) => {
|
||||
task.subtasks?.forEach((st) => {
|
||||
acc.total++;
|
||||
@@ -113,7 +106,7 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
result: {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: result.tasks as Task[],
|
||||
tasks: tasks as Task[],
|
||||
filter: status || 'all',
|
||||
stats: {
|
||||
total: totalTasks,
|
||||
@@ -138,14 +131,14 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
}
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot,
|
||||
tag: result.tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-tasks: ${error.message}`);
|
||||
log.error(`Error in get-tasks: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
@@ -154,7 +147,7 @@ export function registerGetTasksTool(server: FastMCP) {
|
||||
message: `Failed to get tasks: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
1
apps/mcp/tests/fixtures/task-fixtures.ts
vendored
Symbolic link
1
apps/mcp/tests/fixtures/task-fixtures.ts
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../cli/tests/fixtures/task-fixtures.ts
|
||||
244
apps/mcp/tests/integration/tools/get-tasks.tool.test.ts
Normal file
244
apps/mcp/tests/integration/tools/get-tasks.tool.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for get_tasks MCP tool
|
||||
*
|
||||
* Tests the get_tasks MCP tool using the MCP inspector CLI.
|
||||
* This approach is simpler than a custom JSON-RPC client.
|
||||
*
|
||||
* @integration
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTask, createTasksFile } from '../../fixtures/task-fixtures';
|
||||
|
||||
describe('get_tasks MCP tool', () => {
|
||||
let testDir: string;
|
||||
let tasksPath: string;
|
||||
let cliPath: string;
|
||||
let mcpServerPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-mcp-get-tasks-'));
|
||||
process.chdir(testDir);
|
||||
|
||||
cliPath = path.resolve(__dirname, '../../../../../dist/task-master.js');
|
||||
mcpServerPath = path.resolve(
|
||||
__dirname,
|
||||
'../../../../../dist/mcp-server.js'
|
||||
);
|
||||
|
||||
// Initialize Task Master in test directory
|
||||
execSync(`node "${cliPath}" init --yes`, {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, TASKMASTER_SKIP_AUTO_UPDATE: '1' }
|
||||
});
|
||||
|
||||
tasksPath = path.join(testDir, '.taskmaster', 'tasks', 'tasks.json');
|
||||
|
||||
// Create initial empty tasks file using fixtures
|
||||
const initialTasks = createTasksFile();
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(initialTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Change back to original directory and cleanup
|
||||
try {
|
||||
const originalDir = path.resolve(__dirname, '../../../../..');
|
||||
process.chdir(originalDir);
|
||||
} catch {
|
||||
process.chdir(os.homedir());
|
||||
}
|
||||
|
||||
if (testDir && fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const writeTasks = (tasksData: any) => {
|
||||
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Call an MCP tool using the inspector CLI
|
||||
* The inspector returns MCP protocol format: { content: [{ type: "text", text: "<json>" }] }
|
||||
*/
|
||||
const callMCPTool = (toolName: string, args: Record<string, any>): any => {
|
||||
const toolArgs = Object.entries(args)
|
||||
.map(([key, value]) => `--tool-arg ${key}=${value}`)
|
||||
.join(' ');
|
||||
|
||||
const output = execSync(
|
||||
`npx @modelcontextprotocol/inspector --cli node "${mcpServerPath}" --method tools/call --tool-name ${toolName} ${toolArgs}`,
|
||||
{ encoding: 'utf-8', stdio: 'pipe' }
|
||||
);
|
||||
|
||||
// Parse the MCP protocol response: { content: [{ type: "text", text: "<json>" }] }
|
||||
const mcpResponse = JSON.parse(output);
|
||||
const resultText = mcpResponse.content[0].text;
|
||||
return JSON.parse(resultText);
|
||||
};
|
||||
|
||||
it('should return empty task list when no tasks exist', () => {
|
||||
const data = callMCPTool('get_tasks', { projectRoot: testDir });
|
||||
|
||||
expect(data.data.tasks).toEqual([]);
|
||||
expect(data.data.stats.total).toBe(0);
|
||||
expect(data.tag).toBe('master');
|
||||
}, 15000);
|
||||
|
||||
it('should get all tasks with correct information', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Setup Environment',
|
||||
description: 'Install and configure',
|
||||
status: 'done',
|
||||
priority: 'high'
|
||||
}),
|
||||
createTask({
|
||||
id: 2,
|
||||
title: 'Write Tests',
|
||||
description: 'Create test suite',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: ['1']
|
||||
}),
|
||||
createTask({
|
||||
id: 3,
|
||||
title: 'Implement Feature',
|
||||
description: 'Build the thing',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: ['2']
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const data = callMCPTool('get_tasks', { projectRoot: testDir });
|
||||
|
||||
expect(data.data.tasks).toHaveLength(3);
|
||||
expect(data.data.tasks[0].title).toBe('Setup Environment');
|
||||
expect(data.data.tasks[1].title).toBe('Write Tests');
|
||||
expect(data.data.tasks[2].title).toBe('Implement Feature');
|
||||
expect(data.data.stats.total).toBe(3);
|
||||
expect(data.data.stats.completed).toBe(1);
|
||||
expect(data.data.stats.inProgress).toBe(1);
|
||||
expect(data.data.stats.pending).toBe(1);
|
||||
}, 15000);
|
||||
|
||||
it('should filter tasks by status', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done Task', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Pending Task 1', status: 'pending' }),
|
||||
createTask({ id: 3, title: 'Pending Task 2', status: 'pending' }),
|
||||
createTask({ id: 4, title: 'In Progress', status: 'in-progress' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const data = callMCPTool('get_tasks', {
|
||||
projectRoot: testDir,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
expect(data.data.tasks).toHaveLength(2);
|
||||
expect(data.data.tasks.every((t: any) => t.status === 'pending')).toBe(
|
||||
true
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
it('should include subtasks when requested', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({
|
||||
id: 1,
|
||||
title: 'Parent Task',
|
||||
status: 'in-progress',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1.1',
|
||||
parentId: '1',
|
||||
title: 'First Subtask',
|
||||
description: 'First Subtask',
|
||||
status: 'done',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
},
|
||||
{
|
||||
id: '1.2',
|
||||
parentId: '1',
|
||||
title: 'Second Subtask',
|
||||
description: 'Second Subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const data = callMCPTool('get_tasks', {
|
||||
projectRoot: testDir,
|
||||
withSubtasks: true
|
||||
});
|
||||
|
||||
expect(data.data.tasks).toHaveLength(1);
|
||||
expect(data.data.tasks[0].subtasks).toHaveLength(2);
|
||||
expect(data.data.stats.subtasks.total).toBe(2);
|
||||
expect(data.data.stats.subtasks.completed).toBe(1);
|
||||
expect(data.data.stats.subtasks.pending).toBe(1);
|
||||
}, 15000);
|
||||
|
||||
it('should calculate statistics correctly', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done 1', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Done 2', status: 'done' }),
|
||||
createTask({ id: 3, title: 'Done 3', status: 'done' }),
|
||||
createTask({ id: 4, title: 'Pending', status: 'pending' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const data = callMCPTool('get_tasks', { projectRoot: testDir });
|
||||
|
||||
expect(data.data.stats.total).toBe(4);
|
||||
expect(data.data.stats.completed).toBe(3);
|
||||
expect(data.data.stats.pending).toBe(1);
|
||||
expect(data.data.stats.completionPercentage).toBe(75);
|
||||
}, 15000);
|
||||
|
||||
it('should handle multiple status filters', () => {
|
||||
const testData = createTasksFile({
|
||||
tasks: [
|
||||
createTask({ id: 1, title: 'Done Task', status: 'done' }),
|
||||
createTask({ id: 2, title: 'Pending Task', status: 'pending' }),
|
||||
createTask({ id: 3, title: 'Blocked Task', status: 'blocked' }),
|
||||
createTask({ id: 4, title: 'In Progress', status: 'in-progress' })
|
||||
]
|
||||
});
|
||||
writeTasks(testData);
|
||||
|
||||
const data = callMCPTool('get_tasks', {
|
||||
projectRoot: testDir,
|
||||
status: 'blocked,pending'
|
||||
});
|
||||
|
||||
expect(data.data.tasks).toHaveLength(2);
|
||||
const statuses = data.data.tasks.map((t: any) => t.status);
|
||||
expect(statuses).toContain('pending');
|
||||
expect(statuses).toContain('blocked');
|
||||
}, 15000);
|
||||
});
|
||||
@@ -1,23 +1,21 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import rootConfig from '../../vitest.config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'tests/',
|
||||
'**/*.test.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.d.ts',
|
||||
'**/mocks/**',
|
||||
'**/fixtures/**',
|
||||
'vitest.config.ts'
|
||||
/**
|
||||
* MCP package Vitest configuration
|
||||
* Extends root config with MCP-specific settings
|
||||
*/
|
||||
export default mergeConfig(
|
||||
rootConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
// MCP-specific test patterns
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -103,13 +103,6 @@ task-master update-subtask --id=<parentId.subtaskId> --prompt="<prompt>" --resea
|
||||
|
||||
Unlike the `update-task` command which replaces task information, the `update-subtask` command _appends_ new information to the existing subtask details, marking it with a timestamp. This is useful for iteratively enhancing subtasks while preserving the original content.
|
||||
|
||||
## Generate Task Files
|
||||
|
||||
```bash
|
||||
# Generate individual task files from tasks.json
|
||||
task-master generate
|
||||
```
|
||||
|
||||
## Set Task Status
|
||||
|
||||
```bash
|
||||
|
||||
@@ -107,9 +107,6 @@ task-master list
|
||||
|
||||
# Show the next task to work on
|
||||
task-master next
|
||||
|
||||
# Generate task files
|
||||
task-master generate
|
||||
```
|
||||
|
||||
## Setting up Cursor AI Integration
|
||||
@@ -178,14 +175,6 @@ Next, ask the agent to generate individual task files:
|
||||
Please generate individual task files from tasks.json
|
||||
```
|
||||
|
||||
The agent will execute:
|
||||
|
||||
```bash
|
||||
task-master generate
|
||||
```
|
||||
|
||||
This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks.
|
||||
|
||||
## AI-Driven Development Workflow
|
||||
|
||||
The Cursor agent is pre-configured (via the rules file) to follow this workflow:
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* generate-task-files.js
|
||||
* Direct function implementation for generating task files from tasks.json
|
||||
*/
|
||||
|
||||
import { generateTaskFiles } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for generateTaskFiles with error handling.
|
||||
*
|
||||
* @param {Object} args - Command arguments containing tasksJsonPath and outputDir.
|
||||
* @param {string} args.tasksJsonPath - Path to the tasks.json file.
|
||||
* @param {string} args.outputDir - Path to the output directory.
|
||||
* @param {string} args.projectRoot - Project root path (for MCP/env fallback)
|
||||
* @param {string} args.tag - Tag for the task (optional)
|
||||
* @param {Object} log - Logger object.
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function generateTaskFilesDirect(args, log) {
|
||||
// Destructure expected args
|
||||
const { tasksJsonPath, outputDir, projectRoot, tag } = args;
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Check if paths were provided
|
||||
if (!tasksJsonPath) {
|
||||
const errorMessage = 'tasksJsonPath is required but was not provided.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage }
|
||||
};
|
||||
}
|
||||
if (!outputDir) {
|
||||
const errorMessage = 'outputDir is required but was not provided.';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: errorMessage }
|
||||
};
|
||||
}
|
||||
|
||||
// Use the provided paths
|
||||
const tasksPath = tasksJsonPath;
|
||||
const resolvedOutputDir = outputDir;
|
||||
|
||||
log.info(`Generating task files from ${tasksPath} to ${resolvedOutputDir}`);
|
||||
|
||||
// Execute core generateTaskFiles function in a separate try/catch
|
||||
try {
|
||||
// Enable silent mode to prevent logs from being written to stdout
|
||||
enableSilentMode();
|
||||
|
||||
// Pass projectRoot and tag so the core respects context
|
||||
generateTaskFiles(tasksPath, resolvedOutputDir, {
|
||||
projectRoot,
|
||||
tag,
|
||||
mcpLog: log
|
||||
});
|
||||
|
||||
// Restore normal logging after task generation
|
||||
disableSilentMode();
|
||||
} catch (genError) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error in generateTaskFiles: ${genError.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'GENERATE_FILES_ERROR', message: genError.message }
|
||||
};
|
||||
}
|
||||
|
||||
// Return success with file paths
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated task files`,
|
||||
tasksPath: tasksPath,
|
||||
outputDir: resolvedOutputDir,
|
||||
taskFiles:
|
||||
'Individual task files have been generated in the output directory'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging if an outer error occurs
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error generating task files: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'GENERATE_TASKS_ERROR',
|
||||
message: error.message || 'Unknown error generating task files'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { parsePRDDirect } from './direct-functions/parse-prd.js';
|
||||
import { updateTasksDirect } from './direct-functions/update-tasks.js';
|
||||
import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
|
||||
import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
|
||||
import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
|
||||
import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
|
||||
import { nextTaskDirect } from './direct-functions/next-task.js';
|
||||
import { expandTaskDirect } from './direct-functions/expand-task.js';
|
||||
@@ -50,7 +49,6 @@ export const directFunctions = new Map([
|
||||
['updateTasksDirect', updateTasksDirect],
|
||||
['updateTaskByIdDirect', updateTaskByIdDirect],
|
||||
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
|
||||
['generateTaskFilesDirect', generateTaskFilesDirect],
|
||||
['setTaskStatusDirect', setTaskStatusDirect],
|
||||
['nextTaskDirect', nextTaskDirect],
|
||||
['expandTaskDirect', expandTaskDirect],
|
||||
@@ -88,7 +86,6 @@ export {
|
||||
updateTasksDirect,
|
||||
updateTaskByIdDirect,
|
||||
updateSubtaskByIdDirect,
|
||||
generateTaskFilesDirect,
|
||||
setTaskStatusDirect,
|
||||
nextTaskDirect,
|
||||
expandTaskDirect,
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
* Tool for adding a dependency to a task
|
||||
*/
|
||||
|
||||
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
import { addDependencyDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the addDependency tool with the MCP server
|
||||
@@ -37,62 +33,65 @@ export function registerAddDependencyTool(server) {
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(
|
||||
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
|
||||
);
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
let tasksJsonPath;
|
||||
execute: withToolContext(
|
||||
'add-dependency',
|
||||
async (args, { log, session }) => {
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
log.info(
|
||||
`Adding dependency for task ${args.id} to depend on ${args.dependsOn}`
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct function with the resolved path
|
||||
const result = await addDependencyDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
dependsOn: args.dependsOn,
|
||||
// Call the direct function with the resolved path
|
||||
const result = await addDependencyDirect(
|
||||
{
|
||||
// Pass the explicitly resolved path
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
// Pass other relevant args
|
||||
id: args.id,
|
||||
dependsOn: args.dependsOn,
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
// Remove context object
|
||||
);
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully added dependency: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to add dependency: ${result.error.message}`);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
errorPrefix: 'Error adding dependency',
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
// Remove context object
|
||||
);
|
||||
|
||||
// Log result
|
||||
if (result.success) {
|
||||
log.info(`Successfully added dependency: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(`Failed to add dependency: ${result.error.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in addDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error adding dependency',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in addDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { addSubtaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -108,13 +108,12 @@ export function registerAddSubtaskTool(server) {
|
||||
log.error(`Failed to add subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error adding subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error adding subtask',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in addSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { addTagDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -83,13 +83,12 @@ export function registerAddTagTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error creating tag',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error creating tag',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in add-tag tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { addTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -88,7 +88,7 @@ export function registerAddTaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
// Call the direct functionP
|
||||
// Call the direct function
|
||||
const result = await addTaskDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
@@ -107,13 +107,12 @@ export function registerAddTaskTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error adding task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error adding task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in add-task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -150,13 +150,12 @@ export function registerAnalyzeProjectComplexityTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error analyzing task complexity',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error analyzing task complexity',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
|
||||
import { clearSubtasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -43,9 +39,11 @@ export function registerClearSubtasksTool(server) {
|
||||
message: "Either 'id' or 'all' parameter must be provided",
|
||||
path: ['id', 'all']
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: withToolContext('clear-subtasks', async (args, context) => {
|
||||
try {
|
||||
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
|
||||
context.log.info(
|
||||
`Clearing subtasks with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
@@ -57,10 +55,10 @@ export function registerClearSubtasksTool(server) {
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
context.log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
context.log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
@@ -75,25 +73,28 @@ export function registerClearSubtasksTool(server) {
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
context.log,
|
||||
{ session: context.session }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Subtasks cleared successfully: ${result.data.message}`);
|
||||
context.log.info(
|
||||
`Subtasks cleared successfully: ${result.data.message}`
|
||||
);
|
||||
} else {
|
||||
log.error(`Failed to clear subtasks: ${result.error.message}`);
|
||||
context.log.error(
|
||||
`Failed to clear subtasks: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error clearing subtasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: context.log,
|
||||
errorPrefix: 'Error clearing subtasks',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in clearSubtasks tool: ${error.message}`);
|
||||
context.log.error(`Error in clearSubtasks tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { complexityReportDirect } from '../core/task-master-core.js';
|
||||
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
|
||||
import { findComplexityReportPath } from '../core/utils/path-utils.js';
|
||||
@@ -71,13 +71,12 @@ export function registerComplexityReportTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error retrieving complexity report',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error retrieving complexity report',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in complexity-report tool: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { copyTagDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -67,13 +67,12 @@ export function registerCopyTagTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error copying tag',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error copying tag',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in copy-tag tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { deleteTagDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -64,13 +64,12 @@ export function registerDeleteTagTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error deleting tag',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error deleting tag',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in delete-tag tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { expandAllTasksDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
findTasksPath,
|
||||
@@ -111,13 +111,12 @@ export function registerExpandAllTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error expanding all tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error expanding all tasks',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Unexpected error in expand_all tool execute: ${error.message}`
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { expandTaskDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
findTasksPath,
|
||||
@@ -94,13 +94,12 @@ export function registerExpandTaskTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error expanding task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error expanding task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in expand-task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
|
||||
import { fixDependenciesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the fixDependencies tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
@@ -27,9 +24,11 @@ export function registerFixDependenciesTool(server) {
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: withToolContext('fix-dependencies', async (args, context) => {
|
||||
try {
|
||||
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);
|
||||
context.log.info(
|
||||
`Fixing dependencies with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
@@ -41,10 +40,10 @@ export function registerFixDependenciesTool(server) {
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
context.log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
context.log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
@@ -56,24 +55,27 @@ export function registerFixDependenciesTool(server) {
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
context.log
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully fixed dependencies: ${result.data.message}`);
|
||||
context.log.info(
|
||||
`Successfully fixed dependencies: ${result.data.message}`
|
||||
);
|
||||
} else {
|
||||
log.error(`Failed to fix dependencies: ${result.error.message}`);
|
||||
context.log.error(
|
||||
`Failed to fix dependencies: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error fixing dependencies',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: context.log,
|
||||
errorPrefix: 'Error fixing dependencies',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in fixDependencies tool: ${error.message}`);
|
||||
context.log.error(`Error in fixDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* tools/generate.js
|
||||
* Tool to generate individual task files from tasks.json
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { generateTaskFilesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Register the generate tool with the MCP server
|
||||
* @param {Object} server - FastMCP server instance
|
||||
*/
|
||||
export function registerGenerateTool(server) {
|
||||
server.addTool({
|
||||
name: 'generate',
|
||||
description:
|
||||
'Generates individual task files in tasks/ directory based on tasks.json',
|
||||
parameters: z.object({
|
||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||
output: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Output directory (default: same directory as tasks file)'),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
|
||||
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const outputDir = args.output
|
||||
? path.resolve(args.projectRoot, args.output)
|
||||
: path.dirname(tasksJsonPath);
|
||||
|
||||
const result = await generateTaskFilesDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
outputDir: outputDir,
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully generated task files: ${result.data.message}`);
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to generate task files: ${result.error?.message || 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error generating task files',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in generate tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// mcp-server/src/tools/get-operation-status.js
|
||||
import { z } from 'zod';
|
||||
import { createErrorResponse, createContentResponse } from './utils.js'; // Assuming these utils exist
|
||||
import { createErrorResponse, createContentResponse } from '@tm/mcp';
|
||||
|
||||
/**
|
||||
* Register the get_operation_status tool.
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { initializeProjectDirect } from '../core/task-master-core.js';
|
||||
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
|
||||
|
||||
@@ -65,13 +65,12 @@ export function registerInitializeProjectTool(server) {
|
||||
|
||||
const result = await initializeProjectDirect(args, log, { session });
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Initialization failed',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Initialization failed',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = `Project initialization tool failed: ${error.message || 'Unknown error'}`;
|
||||
log.error(errorMessage, error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { listTagsDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -62,13 +62,12 @@ export function registerListTagsTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error listing tags',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error listing tags',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in list-tags tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { handleApiResult, createErrorResponse, withToolContext } from '@tm/mcp';
|
||||
import { modelsDirect } from '../core/task-master-core.js';
|
||||
|
||||
/**
|
||||
@@ -83,26 +79,27 @@ export function registerModelsTool(server) {
|
||||
'Custom base URL for providers that support it (e.g., https://api.example.com/v1).'
|
||||
)
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: withToolContext('models', async (args, context) => {
|
||||
try {
|
||||
log.info(`Starting models tool with args: ${JSON.stringify(args)}`);
|
||||
context.log.info(
|
||||
`Starting models tool with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// Use args.projectRoot directly (normalized by withToolContext)
|
||||
const result = await modelsDirect(
|
||||
{ ...args, projectRoot: args.projectRoot },
|
||||
log,
|
||||
{ session }
|
||||
context.log,
|
||||
{ session: context.session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error managing models',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: context.log,
|
||||
errorPrefix: 'Error managing models',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in models tool: ${error.message}`);
|
||||
context.log.error(`Error in models tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
* Tool for moving tasks or subtasks to a new position
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { z } from 'zod';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
import {
|
||||
moveTaskDirect,
|
||||
moveTaskCrossTagDirect
|
||||
moveTaskCrossTagDirect,
|
||||
moveTaskDirect
|
||||
} from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the moveTask tool with the MCP server
|
||||
@@ -83,8 +83,8 @@ export function registerMoveTaskTool(server) {
|
||||
}
|
||||
|
||||
// Use cross-tag move function
|
||||
return handleApiResult(
|
||||
await moveTaskCrossTagDirect(
|
||||
return handleApiResult({
|
||||
result: await moveTaskCrossTagDirect(
|
||||
{
|
||||
sourceIds: args.from,
|
||||
sourceTag: args.fromTag,
|
||||
@@ -98,10 +98,9 @@ export function registerMoveTaskTool(server) {
|
||||
{ session }
|
||||
),
|
||||
log,
|
||||
'Error moving tasks between tags',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
errorPrefix: 'Error moving tasks between tags',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} else {
|
||||
// Within-tag move logic (existing functionality)
|
||||
if (!args.to) {
|
||||
@@ -166,8 +165,8 @@ export function registerMoveTaskTool(server) {
|
||||
}
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
{
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: {
|
||||
moves: results,
|
||||
@@ -176,13 +175,12 @@ export function registerMoveTaskTool(server) {
|
||||
}
|
||||
},
|
||||
log,
|
||||
'Error moving multiple tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
errorPrefix: 'Error moving multiple tasks',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
}
|
||||
return handleApiResult(
|
||||
{
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: {
|
||||
moves: results,
|
||||
@@ -191,14 +189,13 @@ export function registerMoveTaskTool(server) {
|
||||
}
|
||||
},
|
||||
log,
|
||||
'Error moving multiple tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
errorPrefix: 'Error moving multiple tasks',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} else {
|
||||
// Moving a single task
|
||||
return handleApiResult(
|
||||
await moveTaskDirect(
|
||||
return handleApiResult({
|
||||
result: await moveTaskDirect(
|
||||
{
|
||||
sourceId: args.from,
|
||||
destinationId: args.to,
|
||||
@@ -211,10 +208,9 @@ export function registerMoveTaskTool(server) {
|
||||
{ session }
|
||||
),
|
||||
log,
|
||||
'Error moving task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
errorPrefix: 'Error moving task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { nextTaskDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
resolveTasksPath,
|
||||
@@ -82,13 +82,12 @@ export function registerNextTaskTool(server) {
|
||||
);
|
||||
|
||||
log.info(`Next task result: ${result.success ? 'found' : 'none'}`);
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error finding next task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error finding next task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error finding next task: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
withNormalizedProjectRoot,
|
||||
createErrorResponse,
|
||||
checkProgressCapability
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { parsePRDDirect } from '../core/task-master-core.js';
|
||||
import {
|
||||
PRD_FILE,
|
||||
@@ -84,13 +84,12 @@ export function registerParsePRDTool(server) {
|
||||
log,
|
||||
{ session, reportProgress: progressCapability }
|
||||
);
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error parsing PRD',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error parsing PRD',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in parse_prd: ${error.message}`);
|
||||
return createErrorResponse(`Failed to parse PRD: ${error.message}`);
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
* Tool for removing a dependency from a task
|
||||
*/
|
||||
|
||||
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
import { removeDependencyDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the removeDependency tool with the MCP server
|
||||
@@ -35,25 +31,25 @@ export function registerRemoveDependencyTool(server) {
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
execute: withToolContext('remove-dependency', async (args, context) => {
|
||||
try {
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
log.info(
|
||||
context.log.info(
|
||||
`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
// Use args.projectRoot directly (guaranteed by withToolContext)
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
context.log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
context.log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
@@ -67,24 +63,27 @@ export function registerRemoveDependencyTool(server) {
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
context.log
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(`Successfully removed dependency: ${result.data.message}`);
|
||||
context.log.info(
|
||||
`Successfully removed dependency: ${result.data.message}`
|
||||
);
|
||||
} else {
|
||||
log.error(`Failed to remove dependency: ${result.error.message}`);
|
||||
context.log.error(
|
||||
`Failed to remove dependency: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error removing dependency',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: context.log,
|
||||
errorPrefix: 'Error removing dependency',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in removeDependency tool: ${error.message}`);
|
||||
context.log.error(`Error in removeDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { removeSubtaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -89,13 +89,12 @@ export function registerRemoveSubtaskTool(server) {
|
||||
log.error(`Failed to remove subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error removing subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error removing subtask',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in removeSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { removeTaskDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -84,13 +84,12 @@ export function registerRemoveTaskTool(server) {
|
||||
log.error(`Failed to remove task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error removing task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error removing task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in remove-task tool: ${error.message}`);
|
||||
return createErrorResponse(`Failed to remove task: ${error.message}`);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { renameTagDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -61,13 +61,12 @@ export function registerRenameTagTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error renaming tag',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error renaming tag',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in rename-tag tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { researchDirect } from '../core/task-master-core.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
@@ -94,13 +94,12 @@ export function registerResearchTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error performing research',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error performing research',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in research tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { responseLanguageDirect } from '../core/direct-functions/response-language.js';
|
||||
|
||||
export function registerResponseLanguageTool(server) {
|
||||
@@ -36,7 +36,12 @@ export function registerResponseLanguageTool(server) {
|
||||
log,
|
||||
{ session }
|
||||
);
|
||||
return handleApiResult(result, log, 'Error setting response language');
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
errorPrefix: 'Error setting response language',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in response-language tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { rulesDirect } from '../core/direct-functions/rules.js';
|
||||
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
|
||||
|
||||
@@ -49,7 +49,11 @@ export function registerRulesTool(server) {
|
||||
`[rules tool] Executing action: ${args.action} for profiles: ${args.profiles.join(', ')} in ${args.projectRoot}`
|
||||
);
|
||||
const result = await rulesDirect(args, log, { session });
|
||||
return handleApiResult(result, log);
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`[rules tool] Error: ${error.message}`);
|
||||
return createErrorResponse(error.message, { details: error.stack });
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { scopeDownDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -88,13 +88,12 @@ export function registerScopeDownTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error scoping down task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error scoping down task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in scope-down tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { scopeUpDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -88,13 +88,12 @@ export function registerScopeUpTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error scoping up task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error scoping up task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in scope-up tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import {
|
||||
setTaskStatusDirect,
|
||||
nextTaskDirect
|
||||
@@ -113,13 +113,12 @@ export function registerSetTaskStatusTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error setting task status',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error setting task status',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in setTaskStatus tool: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
|
||||
@@ -8,7 +8,6 @@ import { registerParsePRDTool } from './parse-prd.js';
|
||||
import { registerUpdateTool } from './update.js';
|
||||
import { registerUpdateTaskTool } from './update-task.js';
|
||||
import { registerUpdateSubtaskTool } from './update-subtask.js';
|
||||
import { registerGenerateTool } from './generate.js';
|
||||
import { registerNextTaskTool } from './next-task.js';
|
||||
import { registerExpandTaskTool } from './expand-task.js';
|
||||
import { registerAddTaskTool } from './add-task.js';
|
||||
@@ -72,7 +71,6 @@ export const toolRegistry = {
|
||||
next_task: registerNextTaskTool,
|
||||
complexity_report: registerComplexityReportTool,
|
||||
set_task_status: registerSetTaskStatusTool,
|
||||
generate: registerGenerateTool,
|
||||
add_task: registerAddTaskTool,
|
||||
add_subtask: registerAddSubtaskTool,
|
||||
update: registerUpdateTool,
|
||||
@@ -128,7 +126,6 @@ export const standardTools = [
|
||||
'expand_all',
|
||||
'add_subtask',
|
||||
'remove_task',
|
||||
'generate',
|
||||
'add_task',
|
||||
'complexity_report'
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { updateSubtaskByIdDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -83,13 +83,12 @@ export function registerUpdateSubtaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error updating subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error updating subtask',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { updateTaskByIdDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -91,13 +91,12 @@ export function registerUpdateTaskTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error updating task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error updating task',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { updateTasksDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
@@ -88,13 +88,12 @@ export function registerUpdateTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error updating tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error updating tasks',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
createErrorResponse,
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
} from '@tm/mcp';
|
||||
import { useTagDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
|
||||
@@ -59,13 +59,12 @@ export function registerUseTagTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
'Error switching tag',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
log: log,
|
||||
errorPrefix: 'Error switching tag',
|
||||
projectRoot: args.projectRoot
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in use-tag tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
* Tool for validating task dependencies
|
||||
*/
|
||||
|
||||
import { createErrorResponse, handleApiResult, withToolContext } from '@tm/mcp';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
handleApiResult,
|
||||
createErrorResponse,
|
||||
withNormalizedProjectRoot
|
||||
} from './utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
import { validateDependenciesDirect } from '../core/task-master-core.js';
|
||||
import { findTasksPath } from '../core/utils/path-utils.js';
|
||||
import { resolveTag } from '../../../scripts/modules/utils.js';
|
||||
|
||||
/**
|
||||
* Register the validateDependencies tool with the MCP server
|
||||
@@ -29,56 +25,63 @@ export function registerValidateDependenciesTool(server) {
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
}),
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
|
||||
let tasksJsonPath;
|
||||
execute: withToolContext(
|
||||
'validate-dependencies',
|
||||
async (args, { log, session }) => {
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
const resolvedTag = resolveTag({
|
||||
projectRoot: args.projectRoot,
|
||||
tag: args.tag
|
||||
});
|
||||
log.info(
|
||||
`Validating dependencies with args: ${JSON.stringify(args)}`
|
||||
);
|
||||
|
||||
// Use args.projectRoot directly (guaranteed by withToolContext)
|
||||
let tasksJsonPath;
|
||||
try {
|
||||
tasksJsonPath = findTasksPath(
|
||||
{ projectRoot: args.projectRoot, file: args.file },
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await validateDependenciesDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding tasks.json: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
`Failed to find tasks.json: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await validateDependenciesDirect(
|
||||
{
|
||||
tasksJsonPath: tasksJsonPath,
|
||||
if (result.success) {
|
||||
log.info(
|
||||
`Successfully validated dependencies: ${result.data.message}`
|
||||
);
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to validate dependencies: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult({
|
||||
result,
|
||||
log,
|
||||
errorPrefix: 'Error validating dependencies',
|
||||
projectRoot: args.projectRoot,
|
||||
tag: resolvedTag
|
||||
},
|
||||
log
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
log.info(
|
||||
`Successfully validated dependencies: ${result.data.message}`
|
||||
);
|
||||
} else {
|
||||
log.error(`Failed to validate dependencies: ${result.error.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`Error in validateDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error validating dependencies',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in validateDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
15866
package-lock.json
generated
15866
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,7 @@
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"execa": "^8.0.1",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
Generate individual task files from tasks.json.
|
||||
|
||||
## Task File Generation
|
||||
|
||||
Creates separate markdown files for each task, perfect for AI agents or documentation.
|
||||
|
||||
## Execution
|
||||
|
||||
```bash
|
||||
task-master generate
|
||||
```
|
||||
|
||||
## What It Creates
|
||||
|
||||
For each task, generates a file like `task_001.txt`:
|
||||
|
||||
```
|
||||
Task ID: 1
|
||||
Title: Implement user authentication
|
||||
Status: pending
|
||||
Priority: high
|
||||
Dependencies: []
|
||||
Created: 2024-01-15
|
||||
Complexity: 7
|
||||
|
||||
## Description
|
||||
Create a secure user authentication system with login, logout, and session management.
|
||||
|
||||
## Details
|
||||
- Use JWT tokens for session management
|
||||
- Implement secure password hashing
|
||||
- Add remember me functionality
|
||||
- Include password reset flow
|
||||
|
||||
## Test Strategy
|
||||
- Unit tests for auth functions
|
||||
- Integration tests for login flow
|
||||
- Security testing for vulnerabilities
|
||||
- Performance tests for concurrent logins
|
||||
|
||||
## Subtasks
|
||||
1.1 Setup authentication framework (pending)
|
||||
1.2 Create login endpoints (pending)
|
||||
1.3 Implement session management (pending)
|
||||
1.4 Add password reset (pending)
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
Creates structure:
|
||||
```
|
||||
.taskmaster/
|
||||
└── tasks/
|
||||
├── task_001.txt
|
||||
├── task_002.txt
|
||||
├── task_003.txt
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Smart Features
|
||||
|
||||
1. **Consistent Formatting**
|
||||
- Standardized structure
|
||||
- Clear sections
|
||||
- AI-readable format
|
||||
- Markdown compatible
|
||||
|
||||
2. **Contextual Information**
|
||||
- Full task details
|
||||
- Related task references
|
||||
- Progress indicators
|
||||
- Implementation notes
|
||||
|
||||
3. **Incremental Updates**
|
||||
- Only regenerate changed tasks
|
||||
- Preserve custom additions
|
||||
- Track generation timestamp
|
||||
- Version control friendly
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **AI Context**: Provide task context to AI assistants
|
||||
- **Documentation**: Standalone task documentation
|
||||
- **Archival**: Task history preservation
|
||||
- **Sharing**: Send specific tasks to team members
|
||||
- **Review**: Easier task review process
|
||||
|
||||
## Generation Options
|
||||
|
||||
Based on arguments:
|
||||
- Filter by status
|
||||
- Include/exclude completed
|
||||
- Custom templates
|
||||
- Different formats
|
||||
|
||||
## Post-Generation
|
||||
|
||||
```
|
||||
Task File Generation Complete
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Generated: 45 task files
|
||||
Location: .taskmaster/tasks/
|
||||
Total size: 156 KB
|
||||
|
||||
New files: 5
|
||||
Updated files: 12
|
||||
Unchanged: 28
|
||||
|
||||
Ready for:
|
||||
- AI agent consumption
|
||||
- Version control
|
||||
- Team distribution
|
||||
```
|
||||
|
||||
## Integration Benefits
|
||||
|
||||
- Git-trackable task history
|
||||
- Easy task sharing
|
||||
- AI tool compatibility
|
||||
- Offline task access
|
||||
- Backup redundancy
|
||||
@@ -25,7 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"files": ["src", "README.md"],
|
||||
"keywords": ["temporary", "bridge", "migration"],
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"strip-literal": "3.1.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
|
||||
@@ -82,6 +82,12 @@ export type {
|
||||
} from './modules/auth/types.js';
|
||||
export { AuthenticationError } from './modules/auth/types.js';
|
||||
|
||||
// Auth constants
|
||||
export {
|
||||
LOCAL_ONLY_COMMANDS,
|
||||
type LocalOnlyCommand
|
||||
} from './modules/auth/index.js';
|
||||
|
||||
// Brief types
|
||||
export type { Brief } from './modules/briefs/types.js';
|
||||
export type { TagWithStats } from './modules/briefs/services/brief-service.js';
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
OAuthFlowOptions,
|
||||
UserContext
|
||||
} from './types.js';
|
||||
import { checkAuthBlock, type AuthBlockResult } from './command.guard.js';
|
||||
|
||||
/**
|
||||
* Display information for storage context
|
||||
@@ -225,6 +226,41 @@ export class AuthDomain {
|
||||
return `${baseUrl}/home/${context.orgSlug}/briefs/create`;
|
||||
}
|
||||
|
||||
// ========== Command Guards ==========
|
||||
|
||||
/**
|
||||
* Check if a local-only command should be blocked when using API storage
|
||||
*
|
||||
* Local-only commands (like dependency management) are blocked when authenticated
|
||||
* with Hamster and using API storage, since Hamster manages these features remotely.
|
||||
*
|
||||
* @param commandName - Name of the command to check
|
||||
* @param storageType - Current storage type being used
|
||||
* @returns Guard result with blocking decision and context
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await tmCore.auth.guardCommand('add-dependency', tmCore.tasks.getStorageType());
|
||||
* if (result.isBlocked) {
|
||||
* console.log(`Command blocked: ${result.briefName}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async guardCommand(
|
||||
commandName: string,
|
||||
storageType: StorageType
|
||||
): Promise<AuthBlockResult> {
|
||||
const hasValidSession = await this.hasValidSession();
|
||||
const context = this.getContext();
|
||||
|
||||
return checkAuthBlock({
|
||||
hasValidSession,
|
||||
briefName: context?.briefName,
|
||||
storageType,
|
||||
commandName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get web app base URL from environment configuration
|
||||
* @private
|
||||
|
||||
77
packages/tm-core/src/modules/auth/command.guard.ts
Normal file
77
packages/tm-core/src/modules/auth/command.guard.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @fileoverview Command guard - Core logic for blocking local-only commands
|
||||
* Pure business logic - no presentation layer concerns
|
||||
*/
|
||||
|
||||
import type { StorageType } from '../../common/types/index.js';
|
||||
import { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';
|
||||
|
||||
/**
|
||||
* Result from checking if a command should be blocked
|
||||
*/
|
||||
export interface AuthBlockResult {
|
||||
/** Whether the command should be blocked */
|
||||
isBlocked: boolean;
|
||||
/** Brief name if authenticated with Hamster */
|
||||
briefName?: string;
|
||||
/** Command name that was checked */
|
||||
commandName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command is local-only
|
||||
*/
|
||||
export function isLocalOnlyCommand(
|
||||
commandName: string
|
||||
): commandName is LocalOnlyCommand {
|
||||
return LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for auth block check
|
||||
*/
|
||||
export interface AuthBlockParams {
|
||||
/** Whether user has a valid auth session */
|
||||
hasValidSession: boolean;
|
||||
/** Brief name from auth context */
|
||||
briefName?: string;
|
||||
/** Current storage type being used */
|
||||
storageType: StorageType;
|
||||
/** Command name to check */
|
||||
commandName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command should be blocked because user is authenticated with Hamster
|
||||
*
|
||||
* This is pure business logic with dependency injection - returns data only, no display/formatting
|
||||
* Presentation layers (CLI, MCP) should format the response appropriately
|
||||
*
|
||||
* @param params - Auth block parameters
|
||||
* @returns AuthBlockResult with blocking decision and context
|
||||
*/
|
||||
export function checkAuthBlock(params: AuthBlockParams): AuthBlockResult {
|
||||
const { hasValidSession, briefName, storageType, commandName } = params;
|
||||
|
||||
// Only check auth for local-only commands
|
||||
if (!isLocalOnlyCommand(commandName)) {
|
||||
return { isBlocked: false, commandName };
|
||||
}
|
||||
|
||||
// Not authenticated - command is allowed
|
||||
if (!hasValidSession) {
|
||||
return { isBlocked: false, commandName };
|
||||
}
|
||||
|
||||
// Authenticated but using file storage - command is allowed
|
||||
if (storageType !== 'api') {
|
||||
return { isBlocked: false, commandName };
|
||||
}
|
||||
|
||||
// User is authenticated AND using API storage - block the command
|
||||
return {
|
||||
isBlocked: true,
|
||||
briefName: briefName || 'remote brief',
|
||||
commandName
|
||||
};
|
||||
}
|
||||
18
packages/tm-core/src/modules/auth/constants.ts
Normal file
18
packages/tm-core/src/modules/auth/constants.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @fileoverview Auth module constants
|
||||
*/
|
||||
|
||||
/**
|
||||
* Commands that are only available for local file storage
|
||||
* These commands are blocked when using Hamster (API storage)
|
||||
*/
|
||||
export const LOCAL_ONLY_COMMANDS = [
|
||||
'add-dependency',
|
||||
'remove-dependency',
|
||||
'validate-dependencies',
|
||||
'fix-dependencies',
|
||||
'clear-subtasks',
|
||||
'models'
|
||||
] as const;
|
||||
|
||||
export type LocalOnlyCommand = (typeof LOCAL_ONLY_COMMANDS)[number];
|
||||
@@ -26,3 +26,9 @@ export {
|
||||
DEFAULT_AUTH_CONFIG,
|
||||
getAuthConfig
|
||||
} from './config.js';
|
||||
|
||||
// Command guard types and utilities
|
||||
export { isLocalOnlyCommand, type AuthBlockResult } from './command.guard.js';
|
||||
|
||||
// Auth constants
|
||||
export { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';
|
||||
|
||||
385
packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts
Normal file
385
packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for TaskEntity validation
|
||||
* Tests that validation errors are properly thrown with correct error codes
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TaskEntity } from './task.entity.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
|
||||
import type { Task } from '../../../common/types/index.js';
|
||||
|
||||
describe('TaskEntity', () => {
|
||||
describe('validation', () => {
|
||||
it('should create a valid task entity', () => {
|
||||
const validTask: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A valid test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(validTask);
|
||||
|
||||
expect(entity.id).toBe('1');
|
||||
expect(entity.title).toBe('Test Task');
|
||||
expect(entity.description).toBe('A valid test task');
|
||||
expect(entity.status).toBe('pending');
|
||||
expect(entity.priority).toBe('high');
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when id is missing', () => {
|
||||
const invalidTask = {
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as any;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task ID is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when title is missing', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: '',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task title is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when description is missing', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: '',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task description is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when title is only whitespace', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: ' ',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task title is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when description is only whitespace', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: ' ',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task description is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should convert numeric id to string', () => {
|
||||
const taskWithNumericId = {
|
||||
id: 123,
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as any;
|
||||
|
||||
const entity = new TaskEntity(taskWithNumericId);
|
||||
|
||||
expect(entity.id).toBe('123');
|
||||
expect(typeof entity.id).toBe('string');
|
||||
});
|
||||
|
||||
it('should convert dependency ids to strings', () => {
|
||||
const taskWithNumericDeps = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [1, 2, '3'] as any,
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(taskWithNumericDeps);
|
||||
|
||||
expect(entity.dependencies).toEqual(['1', '2', '3']);
|
||||
entity.dependencies.forEach((dep) => {
|
||||
expect(typeof dep).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize subtask ids to strings for parent and numbers for subtask', () => {
|
||||
const taskWithSubtasks = {
|
||||
id: '1',
|
||||
title: 'Parent Task',
|
||||
description: 'A parent task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1' as any,
|
||||
parentId: '1',
|
||||
title: 'Subtask 1',
|
||||
description: 'First subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
},
|
||||
{
|
||||
id: 2 as any,
|
||||
parentId: 1 as any,
|
||||
title: 'Subtask 2',
|
||||
description: 'Second subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
}
|
||||
]
|
||||
} as Task;
|
||||
|
||||
const entity = new TaskEntity(taskWithSubtasks);
|
||||
|
||||
expect(entity.subtasks[0].id).toBe(1);
|
||||
expect(typeof entity.subtasks[0].id).toBe('number');
|
||||
expect(entity.subtasks[0].parentId).toBe('1');
|
||||
expect(typeof entity.subtasks[0].parentId).toBe('string');
|
||||
|
||||
expect(entity.subtasks[1].id).toBe(2);
|
||||
expect(typeof entity.subtasks[1].id).toBe('number');
|
||||
expect(entity.subtasks[1].parentId).toBe('1');
|
||||
expect(typeof entity.subtasks[1].parentId).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromObject', () => {
|
||||
it('should create TaskEntity from plain object', () => {
|
||||
const plainTask: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = TaskEntity.fromObject(plainTask);
|
||||
|
||||
expect(entity).toBeInstanceOf(TaskEntity);
|
||||
expect(entity.id).toBe('1');
|
||||
expect(entity.title).toBe('Test Task');
|
||||
});
|
||||
|
||||
it('should throw validation error for invalid object', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: '',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => TaskEntity.fromObject(invalidTask)).toThrow(TaskMasterError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromArray', () => {
|
||||
it('should create array of TaskEntities from plain objects', () => {
|
||||
const plainTasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Task 1',
|
||||
description: 'First task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Task 2',
|
||||
description: 'Second task',
|
||||
status: 'in-progress',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
];
|
||||
|
||||
const entities = TaskEntity.fromArray(plainTasks);
|
||||
|
||||
expect(entities).toHaveLength(2);
|
||||
expect(entities[0]).toBeInstanceOf(TaskEntity);
|
||||
expect(entities[1]).toBeInstanceOf(TaskEntity);
|
||||
expect(entities[0].id).toBe('1');
|
||||
expect(entities[1].id).toBe('2');
|
||||
});
|
||||
|
||||
it('should throw validation error if any task is invalid', () => {
|
||||
const tasksWithInvalid: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Valid Task',
|
||||
description: 'First task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Invalid Task',
|
||||
description: '', // Invalid - missing description
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
];
|
||||
|
||||
expect(() => TaskEntity.fromArray(tasksWithInvalid)).toThrow(
|
||||
TaskMasterError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON', () => {
|
||||
it('should convert TaskEntity to plain object', () => {
|
||||
const taskData: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: ['2', '3'],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(taskData);
|
||||
const json = entity.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: ['2', '3'],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -170,16 +170,13 @@ export class TaskService {
|
||||
storageType
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Just re-throw user-facing errors without wrapping
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
// These errors are already user-friendly and have appropriate error codes
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log internal errors
|
||||
// Only wrap unknown errors
|
||||
this.logger.error('Failed to get task list', error);
|
||||
throw new TaskMasterError(
|
||||
'Failed to get task list',
|
||||
@@ -205,11 +202,8 @@ export class TaskService {
|
||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||
return await this.storage.loadTask(String(taskId), activeTag);
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -554,11 +548,8 @@ export class TaskService {
|
||||
// Direct update - no AI processing
|
||||
await this.storage.updateTask(taskIdStr, updates, activeTag);
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -744,11 +735,8 @@ export class TaskService {
|
||||
activeTag
|
||||
);
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -788,11 +776,8 @@ export class TaskService {
|
||||
try {
|
||||
return await this.storage.getTagsWithStats();
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,60 +1,57 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import rootConfig from '../../vitest.config';
|
||||
|
||||
// __dirname in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
],
|
||||
exclude: ['node_modules', 'dist', '.git', '.cache'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'tests/',
|
||||
'**/*.test.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.d.ts',
|
||||
'**/mocks/**',
|
||||
'**/fixtures/**',
|
||||
'vitest.config.ts',
|
||||
'src/index.ts'
|
||||
/**
|
||||
* Core package Vitest configuration
|
||||
* Extends root config with core-specific settings including:
|
||||
* - Path aliases for cleaner imports
|
||||
* - Test setup file
|
||||
* - Higher coverage thresholds (80%)
|
||||
*/
|
||||
export default mergeConfig(
|
||||
rootConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
// Core-specific test patterns
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
],
|
||||
thresholds: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
|
||||
// Core-specific setup
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
|
||||
// Higher coverage thresholds for core package
|
||||
coverage: {
|
||||
thresholds: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
mockReset: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/types': path.resolve(__dirname, './src/types'),
|
||||
'@/providers': path.resolve(__dirname, './src/providers'),
|
||||
'@/storage': path.resolve(__dirname, './src/storage'),
|
||||
'@/parser': path.resolve(__dirname, './src/parser'),
|
||||
'@/utils': path.resolve(__dirname, './src/utils'),
|
||||
'@/errors': path.resolve(__dirname, './src/errors')
|
||||
|
||||
// Path aliases for cleaner imports
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/types': path.resolve(__dirname, './src/types'),
|
||||
'@/providers': path.resolve(__dirname, './src/providers'),
|
||||
'@/storage': path.resolve(__dirname, './src/storage'),
|
||||
'@/parser': path.resolve(__dirname, './src/parser'),
|
||||
'@/utils': path.resolve(__dirname, './src/utils'),
|
||||
'@/errors': path.resolve(__dirname, './src/errors')
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3,140 +3,142 @@
|
||||
* Command-line interface for the Task Master CLI
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import boxen from 'boxen';
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
import { log, readJSON } from './utils.js';
|
||||
// Import command registry and utilities from @tm/cli
|
||||
import {
|
||||
registerAllCommands,
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification,
|
||||
restartWithNewVersion,
|
||||
displayError,
|
||||
displayUpgradeNotification,
|
||||
performAutoUpdate,
|
||||
registerAllCommands,
|
||||
restartWithNewVersion,
|
||||
runInteractiveSetup
|
||||
} from '@tm/cli';
|
||||
import { log, readJSON } from './utils.js';
|
||||
|
||||
import {
|
||||
parsePRD,
|
||||
updateTasks,
|
||||
generateTaskFiles,
|
||||
expandTask,
|
||||
expandAllTasks,
|
||||
clearSubtasks,
|
||||
addTask,
|
||||
addSubtask,
|
||||
removeSubtask,
|
||||
addTask,
|
||||
analyzeTaskComplexity,
|
||||
updateTaskById,
|
||||
updateSubtaskById,
|
||||
removeTask,
|
||||
clearSubtasks,
|
||||
expandAllTasks,
|
||||
expandTask,
|
||||
findTaskById,
|
||||
taskExists,
|
||||
moveTask,
|
||||
migrateProject,
|
||||
setResponseLanguage,
|
||||
scopeUpTask,
|
||||
moveTask,
|
||||
parsePRD,
|
||||
removeSubtask,
|
||||
removeTask,
|
||||
scopeDownTask,
|
||||
scopeUpTask,
|
||||
setResponseLanguage,
|
||||
taskExists,
|
||||
updateSubtaskById,
|
||||
updateTaskById,
|
||||
updateTasks,
|
||||
validateStrength
|
||||
} from './task-manager.js';
|
||||
|
||||
import { moveTasksBetweenTags } from './task-manager/move-task.js';
|
||||
|
||||
import {
|
||||
copyTag,
|
||||
createTag,
|
||||
deleteTag,
|
||||
tags,
|
||||
useTag,
|
||||
renameTag,
|
||||
copyTag
|
||||
tags,
|
||||
useTag
|
||||
} from './task-manager/tag-management.js';
|
||||
|
||||
import {
|
||||
addDependency,
|
||||
fixDependenciesCommand,
|
||||
removeDependency,
|
||||
validateDependenciesCommand,
|
||||
fixDependenciesCommand
|
||||
validateDependenciesCommand
|
||||
} from './dependency-manager.js';
|
||||
|
||||
import { checkAndBlockIfAuthenticated } from '@tm/cli';
|
||||
import { LOCAL_ONLY_COMMANDS } from '@tm/core';
|
||||
|
||||
import {
|
||||
isApiKeySet,
|
||||
getDebugFlag,
|
||||
ConfigurationError,
|
||||
isConfigFilePresent,
|
||||
getDefaultNumTasks
|
||||
getDebugFlag,
|
||||
getDefaultNumTasks,
|
||||
isApiKeySet,
|
||||
isConfigFilePresent
|
||||
} from './config-manager.js';
|
||||
|
||||
import { CUSTOM_PROVIDERS } from '@tm/core';
|
||||
|
||||
import {
|
||||
COMPLEXITY_REPORT_FILE,
|
||||
TASKMASTER_TASKS_FILE,
|
||||
TASKMASTER_DOCS_DIR
|
||||
TASKMASTER_DOCS_DIR,
|
||||
TASKMASTER_TASKS_FILE
|
||||
} from '../../src/constants/paths.js';
|
||||
|
||||
import { initTaskMaster } from '../../src/task-master.js';
|
||||
|
||||
import {
|
||||
displayBanner,
|
||||
displayHelp,
|
||||
displayComplexityReport,
|
||||
getStatusWithColor,
|
||||
confirmTaskOverwrite,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator,
|
||||
displayModelConfiguration,
|
||||
displayAvailableModels,
|
||||
displayApiKeyStatus,
|
||||
displayTaggedTasksFYI,
|
||||
displayCurrentTagIndicator,
|
||||
displayCrossTagDependencyError,
|
||||
displaySubtaskMoveError,
|
||||
displayInvalidTagCombinationError,
|
||||
displayDependencyValidationHints
|
||||
} from './ui.js';
|
||||
import {
|
||||
confirmProfilesRemove,
|
||||
confirmRemoveAllRemainingProfiles
|
||||
} from '../../src/ui/confirm.js';
|
||||
import {
|
||||
wouldRemovalLeaveNoProfiles,
|
||||
getInstalledProfiles
|
||||
getInstalledProfiles,
|
||||
wouldRemovalLeaveNoProfiles
|
||||
} from '../../src/utils/profiles.js';
|
||||
import {
|
||||
confirmTaskOverwrite,
|
||||
displayApiKeyStatus,
|
||||
displayAvailableModels,
|
||||
displayBanner,
|
||||
displayComplexityReport,
|
||||
displayCrossTagDependencyError,
|
||||
displayCurrentTagIndicator,
|
||||
displayDependencyValidationHints,
|
||||
displayHelp,
|
||||
displayInvalidTagCombinationError,
|
||||
displayModelConfiguration,
|
||||
displaySubtaskMoveError,
|
||||
displayTaggedTasksFYI,
|
||||
getStatusWithColor,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator
|
||||
} from './ui.js';
|
||||
|
||||
import { initializeProject } from '../init.js';
|
||||
import {
|
||||
getModelConfiguration,
|
||||
getAvailableModelsList,
|
||||
setModel,
|
||||
getApiKeyStatusReport
|
||||
} from './task-manager/models.js';
|
||||
import {
|
||||
isValidRulesAction,
|
||||
RULES_ACTIONS,
|
||||
RULES_SETUP_ACTION
|
||||
} from '../../src/constants/rules-actions.js';
|
||||
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
|
||||
import { syncTasksToReadme } from './sync-readme.js';
|
||||
import { RULE_PROFILES } from '../../src/constants/profiles.js';
|
||||
import {
|
||||
convertAllRulesToProfileRules,
|
||||
removeProfileRules,
|
||||
isValidProfile,
|
||||
getRulesProfile
|
||||
} from '../../src/utils/rule-transformer.js';
|
||||
RULES_ACTIONS,
|
||||
RULES_SETUP_ACTION,
|
||||
isValidRulesAction
|
||||
} from '../../src/constants/rules-actions.js';
|
||||
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
|
||||
import {
|
||||
runInteractiveProfilesSetup,
|
||||
generateProfileSummary,
|
||||
categorizeProfileResults,
|
||||
categorizeRemovalResults,
|
||||
generateProfileRemovalSummary,
|
||||
categorizeRemovalResults
|
||||
generateProfileSummary,
|
||||
runInteractiveProfilesSetup
|
||||
} from '../../src/utils/profiles.js';
|
||||
import {
|
||||
convertAllRulesToProfileRules,
|
||||
getRulesProfile,
|
||||
isValidProfile,
|
||||
removeProfileRules
|
||||
} from '../../src/utils/rule-transformer.js';
|
||||
import { initializeProject } from '../init.js';
|
||||
import { syncTasksToReadme } from './sync-readme.js';
|
||||
import {
|
||||
getApiKeyStatusReport,
|
||||
getAvailableModelsList,
|
||||
getModelConfiguration,
|
||||
setModel
|
||||
} from './task-manager/models.js';
|
||||
|
||||
/**
|
||||
* Configure and register CLI commands
|
||||
@@ -155,6 +157,23 @@ function registerCommands(programInstance) {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Add global command guard for local-only commands
|
||||
programInstance.hook('preAction', async (thisCommand, actionCommand) => {
|
||||
const commandName = actionCommand.name();
|
||||
|
||||
// Only check if it's a local-only command
|
||||
if (LOCAL_ONLY_COMMANDS.includes(commandName)) {
|
||||
const taskMaster = initTaskMaster(actionCommand.opts());
|
||||
const isBlocked = await checkAndBlockIfAuthenticated(
|
||||
commandName,
|
||||
taskMaster.getProjectRoot()
|
||||
);
|
||||
if (isBlocked) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// parse-prd command
|
||||
programInstance
|
||||
.command('parse-prd')
|
||||
@@ -1000,42 +1019,6 @@ function registerCommands(programInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
// generate command
|
||||
programInstance
|
||||
.command('generate')
|
||||
.description('Generate task files from tasks.json')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option(
|
||||
'-o, --output <dir>',
|
||||
'Output directory',
|
||||
path.dirname(TASKMASTER_TASKS_FILE)
|
||||
)
|
||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||
.action(async (options) => {
|
||||
// Initialize TaskMaster
|
||||
const taskMaster = initTaskMaster({
|
||||
tasksPath: options.file || true,
|
||||
tag: options.tag
|
||||
});
|
||||
|
||||
const outputDir = options.output;
|
||||
const tag = taskMaster.getCurrentTag();
|
||||
|
||||
console.log(
|
||||
chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`)
|
||||
);
|
||||
console.log(chalk.blue(`Output directory: ${outputDir}`));
|
||||
|
||||
await generateTaskFiles(taskMaster.getTasksPath(), outputDir, {
|
||||
projectRoot: taskMaster.getProjectRoot(),
|
||||
tag
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Register All Commands from @tm/cli
|
||||
// ========================================
|
||||
@@ -2561,11 +2544,8 @@ ${result.result}
|
||||
|
||||
try {
|
||||
// Read data once for checks and confirmation
|
||||
const data = readJSON(
|
||||
taskMaster.getTasksPath(),
|
||||
taskMaster.getProjectRoot(),
|
||||
tag
|
||||
);
|
||||
const tasksPath = taskMaster.getTasksPath();
|
||||
const data = readJSON(tasksPath, taskMaster.getProjectRoot(), tag);
|
||||
if (!data || !data.tasks) {
|
||||
console.error(
|
||||
chalk.red(`Error: No valid tasks found in ${tasksPath}`)
|
||||
@@ -3340,33 +3320,6 @@ Examples:
|
||||
console.log('\n' + chalk.yellow.bold('Next Steps:'));
|
||||
result.tips.forEach((t) => console.log(chalk.white(` • ${t}`)));
|
||||
}
|
||||
|
||||
// Check if source tag still contains tasks before regenerating files
|
||||
const tasksData = readJSON(
|
||||
taskMaster.getTasksPath(),
|
||||
taskMaster.getProjectRoot(),
|
||||
sourceTag
|
||||
);
|
||||
const sourceTagHasTasks =
|
||||
tasksData &&
|
||||
Array.isArray(tasksData.tasks) &&
|
||||
tasksData.tasks.length > 0;
|
||||
|
||||
// Generate task files for the affected tags
|
||||
await generateTaskFiles(
|
||||
taskMaster.getTasksPath(),
|
||||
path.dirname(taskMaster.getTasksPath()),
|
||||
{ tag: toTag, projectRoot: taskMaster.getProjectRoot() }
|
||||
);
|
||||
|
||||
// Only regenerate source tag files if it still contains tasks
|
||||
if (sourceTagHasTasks) {
|
||||
await generateTaskFiles(
|
||||
taskMaster.getTasksPath(),
|
||||
path.dirname(taskMaster.getTasksPath()),
|
||||
{ tag: sourceTag, projectRoot: taskMaster.getProjectRoot() }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to handle within-tag move logic
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
|
||||
import { displayBanner } from './ui.js';
|
||||
|
||||
import generateTaskFiles from './task-manager/generate-task-files.js';
|
||||
|
||||
/**
|
||||
* Structured error class for dependency operations
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,6 @@ import { findTaskById } from './utils.js';
|
||||
import parsePRD from './task-manager/parse-prd/index.js';
|
||||
import updateTasks from './task-manager/update-tasks.js';
|
||||
import updateTaskById from './task-manager/update-task-by-id.js';
|
||||
import generateTaskFiles from './task-manager/generate-task-files.js';
|
||||
import setTaskStatus from './task-manager/set-task-status.js';
|
||||
import updateSingleTaskStatus from './task-manager/update-single-task-status.js';
|
||||
import listTasks from './task-manager/list-tasks.js';
|
||||
@@ -40,7 +39,6 @@ export {
|
||||
updateTasks,
|
||||
updateTaskById,
|
||||
updateSubtaskById,
|
||||
generateTaskFiles,
|
||||
setTaskStatus,
|
||||
updateSingleTaskStatus,
|
||||
listTasks,
|
||||
|
||||
@@ -27,7 +27,6 @@ import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import ContextGatherer from '../utils/contextGatherer.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import {
|
||||
TASK_PRIORITY_OPTIONS,
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { log, readJSON } from '../utils.js';
|
||||
import { formatDependenciesWithStatus } from '../ui.js';
|
||||
import { validateAndFixDependencies } from '../dependency-manager.js';
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
|
||||
/**
|
||||
* Generate individual task files from tasks.json
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string} outputDir - Output directory for task files
|
||||
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
|
||||
* @param {string} [options.projectRoot] - Project root path
|
||||
* @param {string} [options.tag] - Tag for the task
|
||||
* @param {Object} [options.mcpLog] - MCP logger object
|
||||
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
|
||||
*/
|
||||
function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
try {
|
||||
const isMcpMode = !!options?.mcpLog;
|
||||
const { projectRoot, tag } = options;
|
||||
|
||||
// 1. Read the raw data structure, ensuring we have all tags.
|
||||
// We call readJSON without a specific tag to get the resolved default view,
|
||||
// which correctly contains the full structure in `_rawTaggedData`.
|
||||
const resolvedData = readJSON(tasksPath, projectRoot, tag);
|
||||
if (!resolvedData) {
|
||||
throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
|
||||
}
|
||||
// Prioritize the _rawTaggedData if it exists, otherwise use the data as is.
|
||||
const rawData = resolvedData._rawTaggedData || resolvedData;
|
||||
|
||||
// 2. Determine the target tag we need to generate files for.
|
||||
const tagData = rawData[tag];
|
||||
|
||||
if (!tagData || !tagData.tasks) {
|
||||
throw new Error(`Tag '${tag}' not found or has no tasks in the data.`);
|
||||
}
|
||||
const tasksForGeneration = tagData.tasks;
|
||||
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
log(
|
||||
'info',
|
||||
`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${tag}'`
|
||||
);
|
||||
|
||||
// 3. Validate dependencies using the FULL, raw data structure to prevent data loss.
|
||||
validateAndFixDependencies(
|
||||
rawData, // Pass the entire object with all tags
|
||||
tasksPath,
|
||||
projectRoot,
|
||||
tag // Provide the current tag context for the operation
|
||||
);
|
||||
|
||||
const allTasksInTag = tagData.tasks;
|
||||
const validTaskIds = allTasksInTag.map((task) => task.id);
|
||||
|
||||
// Cleanup orphaned task files
|
||||
log('info', 'Checking for orphaned task files to clean up...');
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir);
|
||||
// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const masterFilePattern = /^task_(\d+)\.txt$/;
|
||||
const taggedFilePattern = new RegExp(`^task_(\\d+)_${tag}\\.txt$`);
|
||||
|
||||
const orphanedFiles = files.filter((file) => {
|
||||
let match = null;
|
||||
let fileTaskId = null;
|
||||
|
||||
// Check if file belongs to current tag
|
||||
if (tag === 'master') {
|
||||
match = file.match(masterFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up master files when processing master tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
} else {
|
||||
match = file.match(taggedFilePattern);
|
||||
if (match) {
|
||||
fileTaskId = parseInt(match[1], 10);
|
||||
// Only clean up files for the current tag
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (orphanedFiles.length > 0) {
|
||||
log(
|
||||
'info',
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${tag}'`
|
||||
);
|
||||
orphanedFiles.forEach((file) => {
|
||||
const filePath = path.join(outputDir, file);
|
||||
fs.unlinkSync(filePath);
|
||||
});
|
||||
} else {
|
||||
log('info', 'No orphaned task files found.');
|
||||
}
|
||||
} catch (err) {
|
||||
log('warn', `Error cleaning up orphaned task files: ${err.message}`);
|
||||
}
|
||||
|
||||
// Generate task files for the target tag
|
||||
log('info', `Generating individual task files for tag '${tag}'...`);
|
||||
tasksForGeneration.forEach((task) => {
|
||||
// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
|
||||
const taskFileName =
|
||||
tag === 'master'
|
||||
? `task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
: `task_${task.id.toString().padStart(3, '0')}_${tag}.txt`;
|
||||
|
||||
const taskPath = path.join(outputDir, taskFileName);
|
||||
|
||||
let content = `# Task ID: ${task.id}\n`;
|
||||
content += `# Title: ${task.title}\n`;
|
||||
content += `# Status: ${task.status || 'pending'}\n`;
|
||||
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
|
||||
} else {
|
||||
content += '# Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `# Priority: ${task.priority || 'medium'}\n`;
|
||||
content += `# Description: ${task.description || ''}\n`;
|
||||
content += '# Details:\n';
|
||||
content += (task.details || '')
|
||||
.split('\n')
|
||||
.map((line) => line)
|
||||
.join('\n');
|
||||
content += '\n\n';
|
||||
content += '# Test Strategy:\n';
|
||||
content += (task.testStrategy || '')
|
||||
.split('\n')
|
||||
.map((line) => line)
|
||||
.join('\n');
|
||||
content += '\n';
|
||||
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
content += '\n# Subtasks:\n';
|
||||
task.subtasks.forEach((subtask) => {
|
||||
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
|
||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||
const subtaskDeps = subtask.dependencies
|
||||
.map((depId) =>
|
||||
typeof depId === 'number'
|
||||
? `${task.id}.${depId}`
|
||||
: depId.toString()
|
||||
)
|
||||
.join(', ');
|
||||
content += `### Dependencies: ${subtaskDeps}\n`;
|
||||
} else {
|
||||
content += '### Dependencies: None\n';
|
||||
}
|
||||
content += `### Description: ${subtask.description || ''}\n`;
|
||||
content += '### Details:\n';
|
||||
content += (subtask.details || '')
|
||||
.split('\n')
|
||||
.map((line) => line)
|
||||
.join('\n');
|
||||
content += '\n\n';
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(taskPath, content);
|
||||
});
|
||||
|
||||
log(
|
||||
'success',
|
||||
`All ${tasksForGeneration.length} tasks for tag '${tag}' have been generated into '${outputDir}'.`
|
||||
);
|
||||
|
||||
if (isMcpMode) {
|
||||
return {
|
||||
success: true,
|
||||
count: tasksForGeneration.length,
|
||||
directory: outputDir
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `Error generating task files: ${error.message}`);
|
||||
if (!options?.mcpLog) {
|
||||
console.error(chalk.red(`Error generating task files: ${error.message}`));
|
||||
if (getDebugFlag()) {
|
||||
console.error(error);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default generateTaskFiles;
|
||||
@@ -1,7 +1,6 @@
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { readJSON, writeJSON, log, findTaskById } from '../utils.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import taskExists from './task-exists.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,6 @@ import { displayBanner } from '../ui.js';
|
||||
import { validateTaskDependencies } from '../dependency-manager.js';
|
||||
import { getDebugFlag } from '../config-manager.js';
|
||||
import updateSingleTaskStatus from './update-single-task-status.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import {
|
||||
isValidTaskStatus,
|
||||
TASK_STATUS_OPTIONS
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { generateTextService } from '../ai-services-unified.js';
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { ContextGatherer } from '../utils/contextGatherer.js';
|
||||
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
|
||||
import { tryUpdateViaRemote } from '@tm/bridge';
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
|
||||
import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
|
||||
import { getPromptManager } from '../prompt-manager.js';
|
||||
import generateTaskFiles from './generate-task-files.js';
|
||||
import { generateObjectService } from '../ai-services-unified.js';
|
||||
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
|
||||
import { getModelConfiguration } from './models.js';
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
getToolCounts,
|
||||
getToolCategories
|
||||
getToolCategories,
|
||||
getToolCounts
|
||||
} from '../../mcp-server/src/tools/tool-registry.js';
|
||||
|
||||
/**
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
*/
|
||||
export const EXPECTED_TOOL_COUNTS = {
|
||||
core: 7,
|
||||
standard: 15,
|
||||
total: 44
|
||||
standard: 14,
|
||||
total: 43
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// --- Define mock functions ---
|
||||
const mockMoveTasksBetweenTags = jest.fn();
|
||||
const mockMoveTask = jest.fn();
|
||||
const mockGenerateTaskFiles = jest.fn();
|
||||
const mockLog = jest.fn();
|
||||
|
||||
// --- Setup mocks using unstable_mockModule ---
|
||||
@@ -17,13 +16,6 @@ jest.unstable_mockModule(
|
||||
})
|
||||
);
|
||||
|
||||
jest.unstable_mockModule(
|
||||
'../../../scripts/modules/task-manager/generate-task-files.js',
|
||||
() => ({
|
||||
default: mockGenerateTaskFiles
|
||||
})
|
||||
);
|
||||
|
||||
jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
|
||||
log: mockLog,
|
||||
readJSON: jest.fn(),
|
||||
@@ -58,7 +50,7 @@ jest.unstable_mockModule('chalk', () => ({
|
||||
}));
|
||||
|
||||
// --- Import modules (AFTER mock setup) ---
|
||||
let moveTaskModule, generateTaskFilesModule, utilsModule, chalk;
|
||||
let moveTaskModule, utilsModule, chalk;
|
||||
|
||||
describe('Cross-Tag Move CLI Integration', () => {
|
||||
// Setup dynamic imports before tests run
|
||||
@@ -66,9 +58,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
moveTaskModule = await import(
|
||||
'../../../scripts/modules/task-manager/move-task.js'
|
||||
);
|
||||
generateTaskFilesModule = await import(
|
||||
'../../../scripts/modules/task-manager/generate-task-files.js'
|
||||
);
|
||||
utilsModule = await import('../../../scripts/modules/utils.js');
|
||||
chalk = (await import('chalk')).default;
|
||||
});
|
||||
@@ -176,18 +165,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
console.log('Next Steps:');
|
||||
result.tips.forEach((t) => console.log(` • ${t}`));
|
||||
}
|
||||
|
||||
// Generate task files for both tags
|
||||
await generateTaskFilesModule.default(
|
||||
tasksPath,
|
||||
path.dirname(tasksPath),
|
||||
{ tag: sourceTag }
|
||||
);
|
||||
await generateTaskFilesModule.default(
|
||||
tasksPath,
|
||||
path.dirname(tasksPath),
|
||||
{ tag: toTag }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
// Print ID collision guidance similar to CLI help block
|
||||
@@ -271,7 +248,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
it('should move task without dependencies successfully', async () => {
|
||||
// Mock successful cross-tag move
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '2',
|
||||
@@ -324,7 +300,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
it('should move task with dependencies when --with-dependencies is used', async () => {
|
||||
// Mock successful cross-tag move with dependencies
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
@@ -350,7 +325,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
it('should break dependencies when --ignore-dependencies is used', async () => {
|
||||
// Mock successful cross-tag move with dependency breaking
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1',
|
||||
@@ -376,7 +350,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
it('should create target tag if it does not exist', async () => {
|
||||
// Mock successful cross-tag move to new tag
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '2',
|
||||
@@ -567,24 +540,11 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
ignoreDependencies: false
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that generateTaskFiles was called for both tags
|
||||
expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
|
||||
expect.stringContaining('.taskmaster/tasks/tasks.json'),
|
||||
expect.stringContaining('.taskmaster/tasks'),
|
||||
{ tag: 'master' }
|
||||
);
|
||||
expect(generateTaskFilesModule.default).toHaveBeenCalledWith(
|
||||
expect.stringContaining('.taskmaster/tasks/tasks.json'),
|
||||
expect.stringContaining('.taskmaster/tasks'),
|
||||
{ tag: 'in-progress' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should move multiple tasks with comma-separated IDs successfully', async () => {
|
||||
// Mock successful cross-tag move for multiple tasks
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: '1,2,3',
|
||||
@@ -604,19 +564,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
ignoreDependencies: undefined
|
||||
}
|
||||
);
|
||||
|
||||
// Verify task files are generated for both tags
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalledTimes(2);
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
expect.stringContaining('.taskmaster/tasks'),
|
||||
{ tag: 'backlog' }
|
||||
);
|
||||
expect(mockGenerateTaskFiles).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tasks.json'),
|
||||
expect.stringContaining('.taskmaster/tasks'),
|
||||
{ tag: 'in-progress' }
|
||||
);
|
||||
});
|
||||
|
||||
// Note: --force flag is no longer supported for cross-tag moves
|
||||
@@ -710,7 +657,6 @@ describe('Cross-Tag Move CLI Integration', () => {
|
||||
it('should handle whitespace in comma-separated task IDs', async () => {
|
||||
// Mock successful cross-tag move with whitespace
|
||||
mockMoveTasksBetweenTags.mockResolvedValue(undefined);
|
||||
mockGenerateTaskFiles.mockResolvedValue(undefined);
|
||||
|
||||
const options = {
|
||||
from: ' 1 , 2 , 3 ', // Whitespace around IDs and commas
|
||||
|
||||
@@ -111,7 +111,6 @@ const mockExpandTask = jest
|
||||
}
|
||||
);
|
||||
|
||||
const mockGenerateTaskFiles = jest.fn().mockResolvedValue(true);
|
||||
const mockFindTaskById = jest.fn();
|
||||
const mockTaskExists = jest.fn().mockReturnValue(true);
|
||||
|
||||
@@ -155,7 +154,6 @@ jest.mock('../../../scripts/modules/ai-services-unified.js', () => ({
|
||||
// Mock task-manager.js to avoid real operations
|
||||
jest.mock('../../../scripts/modules/task-manager.js', () => ({
|
||||
expandTask: mockExpandTask,
|
||||
generateTaskFiles: mockGenerateTaskFiles,
|
||||
findTaskById: mockFindTaskById,
|
||||
taskExists: mockTaskExists
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -106,13 +106,6 @@ jest.unstable_mockModule('../../scripts/modules/dependency-manager.js', () => ({
|
||||
})
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule(
|
||||
'../../scripts/modules/task-manager/generate-task-files.js',
|
||||
() => ({
|
||||
default: jest.fn().mockResolvedValue()
|
||||
})
|
||||
);
|
||||
|
||||
// Import the modules we'll be testing after mocking
|
||||
const { moveTasksBetweenTags } = await import(
|
||||
'../../scripts/modules/task-manager/move-task.js'
|
||||
|
||||
@@ -71,10 +71,6 @@ jest.mock('../../scripts/modules/ui.js', () => ({
|
||||
displayBanner: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/task-manager.js', () => ({
|
||||
generateTaskFiles: jest.fn()
|
||||
}));
|
||||
|
||||
// Use a temporary path for test files - Jest will clean up the temp directory
|
||||
const TEST_TASKS_PATH = '/tmp/jest-test-tasks.json';
|
||||
|
||||
@@ -991,10 +987,6 @@ describe('Dependency Manager Module', () => {
|
||||
await jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
|
||||
displayBanner: jest.fn()
|
||||
}));
|
||||
await jest.unstable_mockModule(
|
||||
'../../scripts/modules/task-manager/generate-task-files.js',
|
||||
() => ({ default: jest.fn() })
|
||||
);
|
||||
// Set up test data that matches the issue report
|
||||
// Clone fixture data before each test to prevent mutation issues
|
||||
mockReadJSON.mockImplementation(() =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user