Compare commits
2 Commits
v0.12.0
...
fix/211-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36cc324dc3 | ||
|
|
d75a6f3d00 |
6
.changeset/brave-seas-pull.md
Normal file
6
.changeset/brave-seas-pull.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- Fixes shebang issue not allowing task-master to run on certain windows operating systems
|
||||||
|
- Resolves #241 #211 #184 #193
|
||||||
5
.changeset/chubby-moose-stay.md
Normal file
5
.changeset/chubby-moose-stay.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Updates the parameter descriptions for update, update-task and update-subtask to ensure the MCP server correctly reaches for the right update command based on what is being updated -- all tasks, one task, or a subtask.
|
||||||
6
.changeset/moody-pugs-grab.md
Normal file
6
.changeset/moody-pugs-grab.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- Fix `task-master init` polluting codebase with new packages inside `package.json` and modifying project `README`
|
||||||
|
- Now only initializes with cursor rules, windsurf rules, mcp.json, scripts/example_prd.txt, .gitignore modifications, and `README-task-master.md`
|
||||||
5
.changeset/polite-candles-follow.md
Normal file
5
.changeset/polite-candles-follow.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``
|
||||||
5
.changeset/true-adults-build.md
Normal file
5
.changeset/true-adults-build.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed a bug that prevented the task-master from running in a Linux container
|
||||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,35 +1,5 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
## 0.12.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#253](https://github.com/eyaltoledano/claude-task-master/pull/253) [`b2ccd60`](https://github.com/eyaltoledano/claude-task-master/commit/b2ccd605264e47a61451b4c012030ee29011bb40) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``
|
|
||||||
|
|
||||||
- [#267](https://github.com/eyaltoledano/claude-task-master/pull/267) [`c17d912`](https://github.com/eyaltoledano/claude-task-master/commit/c17d912237e6caaa2445e934fc48cd4841abf056) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve PRD parsing prompt with structured analysis and clearer task generation guidelines. We are testing a new prompt - please provide feedback on your experience.
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#243](https://github.com/eyaltoledano/claude-task-master/pull/243) [`454a1d9`](https://github.com/eyaltoledano/claude-task-master/commit/454a1d9d37439c702656eedc0702c2f7a4451517) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - - Fixes shebang issue not allowing task-master to run on certain windows operating systems
|
|
||||||
|
|
||||||
- Resolves #241 #211 #184 #193
|
|
||||||
|
|
||||||
- [#268](https://github.com/eyaltoledano/claude-task-master/pull/268) [`3e872f8`](https://github.com/eyaltoledano/claude-task-master/commit/3e872f8afbb46cd3978f3852b858c233450b9f33) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix remove-task command to handle multiple comma-separated task IDs
|
|
||||||
|
|
||||||
- [#239](https://github.com/eyaltoledano/claude-task-master/pull/239) [`6599cb0`](https://github.com/eyaltoledano/claude-task-master/commit/6599cb0bf9eccecab528207836e9d45b8536e5c2) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Updates the parameter descriptions for update, update-task and update-subtask to ensure the MCP server correctly reaches for the right update command based on what is being updated -- all tasks, one task, or a subtask.
|
|
||||||
|
|
||||||
- [#272](https://github.com/eyaltoledano/claude-task-master/pull/272) [`3aee9bc`](https://github.com/eyaltoledano/claude-task-master/commit/3aee9bc840eb8f31230bd1b761ed156b261cabc4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance the `parsePRD` to include `--append` flag. This flag allows users to append the parsed PRD to an existing file, making it easier to manage multiple PRD files without overwriting existing content.
|
|
||||||
|
|
||||||
- [#264](https://github.com/eyaltoledano/claude-task-master/pull/264) [`ff8e75c`](https://github.com/eyaltoledano/claude-task-master/commit/ff8e75cded91fb677903040002626f7a82fd5f88) Thanks [@joedanz](https://github.com/joedanz)! - Add quotes around numeric env vars in mcp.json (Windsurf, etc.)
|
|
||||||
|
|
||||||
- [#248](https://github.com/eyaltoledano/claude-task-master/pull/248) [`d99fa00`](https://github.com/eyaltoledano/claude-task-master/commit/d99fa00980fc61695195949b33dcda7781006f90) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - - Fix `task-master init` polluting codebase with new packages inside `package.json` and modifying project `README`
|
|
||||||
|
|
||||||
- Now only initializes with cursor rules, windsurf rules, mcp.json, scripts/example_prd.txt, .gitignore modifications, and `README-task-master.md`
|
|
||||||
|
|
||||||
- [#266](https://github.com/eyaltoledano/claude-task-master/pull/266) [`41b979c`](https://github.com/eyaltoledano/claude-task-master/commit/41b979c23963483e54331015a86e7c5079f657e4) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed a bug that prevented the task-master from running in a Linux container
|
|
||||||
|
|
||||||
- [#265](https://github.com/eyaltoledano/claude-task-master/pull/265) [`0eb16d5`](https://github.com/eyaltoledano/claude-task-master/commit/0eb16d5ecbb8402d1318ca9509e9d4087b27fb25) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Remove the need for project name, description, and version. Since we no longer create a package.json for you
|
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ MCP (Model Control Protocol) provides the easiest way to get started with Task M
|
|||||||
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
"PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE",
|
||||||
"MODEL": "claude-3-7-sonnet-20250219",
|
"MODEL": "claude-3-7-sonnet-20250219",
|
||||||
"PERPLEXITY_MODEL": "sonar-pro",
|
"PERPLEXITY_MODEL": "sonar-pro",
|
||||||
"MAX_TOKENS": "64000",
|
"MAX_TOKENS": 64000,
|
||||||
"TEMPERATURE": "0.2",
|
"TEMPERATURE": 0.2,
|
||||||
"DEFAULT_SUBTASKS": "5",
|
"DEFAULT_SUBTASKS": 5,
|
||||||
"DEFAULT_PRIORITY": "medium"
|
"DEFAULT_PRIORITY": "medium"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import os from 'os'; // Import os module for home directory check
|
|||||||
/**
|
/**
|
||||||
* Direct function wrapper for initializing a project.
|
* Direct function wrapper for initializing a project.
|
||||||
* Derives target directory from session, sets CWD, and calls core init logic.
|
* Derives target directory from session, sets CWD, and calls core init logic.
|
||||||
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot)
|
* @param {object} args - Arguments containing project details and options (projectName, projectDescription, yes, etc.)
|
||||||
* @param {object} log - The FastMCP logger instance.
|
* @param {object} log - The FastMCP logger instance.
|
||||||
* @param {object} context - The context object, must contain { session }.
|
* @param {object} context - The context object, must contain { session }.
|
||||||
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
||||||
@@ -92,8 +92,12 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
|||||||
try {
|
try {
|
||||||
// Always force yes: true when called via MCP to avoid interactive prompts
|
// Always force yes: true when called via MCP to avoid interactive prompts
|
||||||
const options = {
|
const options = {
|
||||||
aliases: args.addAliases,
|
name: args.projectName,
|
||||||
|
description: args.projectDescription,
|
||||||
|
version: args.projectVersion,
|
||||||
|
author: args.authorName,
|
||||||
skipInstall: args.skipInstall,
|
skipInstall: args.skipInstall,
|
||||||
|
aliases: args.addAliases,
|
||||||
yes: true // Force yes mode
|
yes: true // Force yes mode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import os from 'os'; // Import os module for home directory check
|
||||||
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
||||||
|
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||||
import {
|
import {
|
||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode
|
disableSilentMode
|
||||||
@@ -122,12 +124,8 @@ export async function parsePRDDirect(args, log, context = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the append flag from args
|
|
||||||
const append = Boolean(args.append) === true;
|
|
||||||
|
|
||||||
// Log key parameters including append flag
|
|
||||||
log.info(
|
log.info(
|
||||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
|
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the logger wrapper for proper logging in the core function
|
// Create the logger wrapper for proper logging in the core function
|
||||||
@@ -159,8 +157,7 @@ export async function parsePRDDirect(args, log, context = {}) {
|
|||||||
numTasks,
|
numTasks,
|
||||||
{
|
{
|
||||||
mcpLog: logWrapper,
|
mcpLog: logWrapper,
|
||||||
session,
|
session
|
||||||
append
|
|
||||||
},
|
},
|
||||||
aiClient,
|
aiClient,
|
||||||
modelConfig
|
modelConfig
|
||||||
@@ -170,18 +167,16 @@ export async function parsePRDDirect(args, log, context = {}) {
|
|||||||
// to return it to the caller
|
// to return it to the caller
|
||||||
if (fs.existsSync(outputPath)) {
|
if (fs.existsSync(outputPath)) {
|
||||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||||
const actionVerb = append ? 'appended' : 'generated';
|
log.info(
|
||||||
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
|
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`
|
||||||
|
);
|
||||||
log.info(message);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
message,
|
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
|
||||||
taskCount: tasksData.tasks?.length || 0,
|
taskCount: tasksData.tasks?.length || 0,
|
||||||
outputPath,
|
outputPath
|
||||||
appended: append
|
|
||||||
},
|
},
|
||||||
fromCache: false // This operation always modifies state and should never be cached
|
fromCache: false // This operation always modifies state and should never be cached
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,23 +3,18 @@
|
|||||||
* Direct function implementation for removing a task
|
* Direct function implementation for removing a task
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { removeTask } from '../../../../scripts/modules/task-manager.js';
|
||||||
removeTask,
|
|
||||||
taskExists
|
|
||||||
} from '../../../../scripts/modules/task-manager.js';
|
|
||||||
import {
|
import {
|
||||||
enableSilentMode,
|
enableSilentMode,
|
||||||
disableSilentMode,
|
disableSilentMode
|
||||||
readJSON
|
|
||||||
} from '../../../../scripts/modules/utils.js';
|
} from '../../../../scripts/modules/utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct function wrapper for removeTask with error handling.
|
* Direct function wrapper for removeTask with error handling.
|
||||||
* Supports removing multiple tasks at once with comma-separated IDs.
|
|
||||||
*
|
*
|
||||||
* @param {Object} args - Command arguments
|
* @param {Object} args - Command arguments
|
||||||
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
||||||
* @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple).
|
* @param {string} args.id - The ID of the task or subtask to remove.
|
||||||
* @param {Object} log - Logger object
|
* @param {Object} log - Logger object
|
||||||
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
|
* @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false }
|
||||||
*/
|
*/
|
||||||
@@ -41,7 +36,8 @@ export async function removeTaskDirect(args, log) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate task ID parameter
|
// Validate task ID parameter
|
||||||
if (!id) {
|
const taskId = id;
|
||||||
|
if (!taskId) {
|
||||||
log.error('Task ID is required');
|
log.error('Task ID is required');
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -53,103 +49,46 @@ export async function removeTaskDirect(args, log) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split task IDs if comma-separated
|
// Skip confirmation in the direct function since it's handled by the client
|
||||||
const taskIdArray = id.split(',').map((taskId) => taskId.trim());
|
log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`);
|
||||||
|
|
||||||
log.info(
|
|
||||||
`Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validate all task IDs exist before proceeding
|
|
||||||
const data = readJSON(tasksJsonPath);
|
|
||||||
if (!data || !data.tasks) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
code: 'INVALID_TASKS_FILE',
|
|
||||||
message: `No valid tasks found in ${tasksJsonPath}`
|
|
||||||
},
|
|
||||||
fromCache: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidTasks = taskIdArray.filter(
|
|
||||||
(taskId) => !taskExists(data.tasks, taskId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (invalidTasks.length > 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
code: 'INVALID_TASK_ID',
|
|
||||||
message: `The following tasks were not found: ${invalidTasks.join(', ')}`
|
|
||||||
},
|
|
||||||
fromCache: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove tasks one by one
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
|
||||||
enableSilentMode();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const taskId of taskIdArray) {
|
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||||
try {
|
enableSilentMode();
|
||||||
const result = await removeTask(tasksJsonPath, taskId);
|
|
||||||
results.push({
|
// Call the core removeTask function using the provided path
|
||||||
taskId,
|
const result = await removeTask(tasksJsonPath, taskId);
|
||||||
success: true,
|
|
||||||
message: result.message,
|
|
||||||
removedTask: result.removedTask
|
|
||||||
});
|
|
||||||
log.info(`Successfully removed task: ${taskId}`);
|
|
||||||
} catch (error) {
|
|
||||||
results.push({
|
|
||||||
taskId,
|
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
log.error(`Error removing task ${taskId}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// Restore normal logging
|
// Restore normal logging
|
||||||
disableSilentMode();
|
disableSilentMode();
|
||||||
}
|
|
||||||
|
|
||||||
// Check if all tasks were successfully removed
|
log.info(`Successfully removed task: ${taskId}`);
|
||||||
const successfulRemovals = results.filter((r) => r.success);
|
|
||||||
const failedRemovals = results.filter((r) => !r.success);
|
|
||||||
|
|
||||||
if (successfulRemovals.length === 0) {
|
// Return the result
|
||||||
// All removals failed
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: result.message,
|
||||||
|
taskId: taskId,
|
||||||
|
tasksPath: tasksJsonPath,
|
||||||
|
removedTask: result.removedTask
|
||||||
|
},
|
||||||
|
fromCache: false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Make sure to restore normal logging even if there's an error
|
||||||
|
disableSilentMode();
|
||||||
|
|
||||||
|
log.error(`Error removing task: ${error.message}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: 'REMOVE_TASK_ERROR',
|
code: error.code || 'REMOVE_TASK_ERROR',
|
||||||
message: 'Failed to remove any tasks',
|
message: error.message || 'Failed to remove task'
|
||||||
details: failedRemovals
|
|
||||||
.map((r) => `${r.taskId}: ${r.error}`)
|
|
||||||
.join('; ')
|
|
||||||
},
|
},
|
||||||
fromCache: false
|
fromCache: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// At least some tasks were removed successfully
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
totalTasks: taskIdArray.length,
|
|
||||||
successful: successfulRemovals.length,
|
|
||||||
failed: failedRemovals.length,
|
|
||||||
results: results,
|
|
||||||
tasksPath: tasksJsonPath
|
|
||||||
},
|
|
||||||
fromCache: false
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ensure silent mode is disabled even if an outer error occurs
|
// Ensure silent mode is disabled even if an outer error occurs
|
||||||
disableSilentMode();
|
disableSilentMode();
|
||||||
|
|||||||
@@ -10,8 +10,32 @@ export function registerInitializeProjectTool(server) {
|
|||||||
server.addTool({
|
server.addTool({
|
||||||
name: 'initialize_project',
|
name: 'initialize_project',
|
||||||
description:
|
description:
|
||||||
'Initializes a new Task Master project structure by calling the core initialization logic. Creates necessary folders and configuration files for Task Master in the current directory.',
|
"Initializes a new Task Master project structure by calling the core initialization logic. Derives target directory from client session. If project details (name, description, author) are not provided, prompts the user or skips if 'yes' flag is true. DO NOT run without parameters.",
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
|
projectName: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'The name for the new project. If not provided, prompt the user for it.'
|
||||||
|
),
|
||||||
|
projectDescription: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'A brief description for the project. If not provided, prompt the user for it.'
|
||||||
|
),
|
||||||
|
projectVersion: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"The initial version for the project (e.g., '0.1.0'). User input not needed unless user requests to override."
|
||||||
|
),
|
||||||
|
authorName: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"The author's name. User input not needed unless user requests to override."
|
||||||
|
),
|
||||||
skipInstall: z
|
skipInstall: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -23,13 +47,15 @@ export function registerInitializeProjectTool(server) {
|
|||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(false)
|
||||||
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
|
.describe(
|
||||||
|
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
|
||||||
|
),
|
||||||
yes: z
|
yes: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(true)
|
.default(false)
|
||||||
.describe(
|
.describe(
|
||||||
'Skip prompts and use default values. Always set to true for MCP tools.'
|
"Skip prompts and use default values or provided arguments. Use true if you wish to skip details like the project name, etc. If the project information required for the initialization is not available or provided by the user, prompt if the user wishes to provide them (name, description, author) or skip them. If the user wishes to skip, set the 'yes' flag to true and do not set any other parameters."
|
||||||
),
|
),
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ export function registerParsePRDTool(server) {
|
|||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.describe('Allow overwriting an existing tasks.json file.'),
|
.describe('Allow overwriting an existing tasks.json file.'),
|
||||||
append: z
|
|
||||||
.boolean()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Append new tasks to existing tasks.json instead of overwriting'
|
|
||||||
),
|
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
.string()
|
.string()
|
||||||
.describe('The directory of the project. Must be absolute path.')
|
.describe('The directory of the project. Must be absolute path.')
|
||||||
@@ -92,8 +86,7 @@ export function registerParsePRDTool(server) {
|
|||||||
input: prdPath,
|
input: prdPath,
|
||||||
output: tasksJsonPath,
|
output: tasksJsonPath,
|
||||||
numTasks: args.numTasks,
|
numTasks: args.numTasks,
|
||||||
force: args.force,
|
force: args.force
|
||||||
append: args.append
|
|
||||||
},
|
},
|
||||||
log,
|
log,
|
||||||
{ session }
|
{ session }
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ export function registerRemoveTaskTool(server) {
|
|||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
id: z
|
id: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"),
|
||||||
"ID(s) of the task(s) or subtask(s) to remove (e.g., '5' or '5.2' or '5,6,7')"
|
|
||||||
),
|
|
||||||
file: z.string().optional().describe('Absolute path to the tasks file'),
|
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
.string()
|
.string()
|
||||||
@@ -37,7 +35,7 @@ export function registerRemoveTaskTool(server) {
|
|||||||
}),
|
}),
|
||||||
execute: async (args, { log, session }) => {
|
execute: async (args, { log, session }) => {
|
||||||
try {
|
try {
|
||||||
log.info(`Removing task(s) with ID(s): ${args.id}`);
|
log.info(`Removing task with ID: ${args.id}`);
|
||||||
|
|
||||||
// Get project root from args or session
|
// Get project root from args or session
|
||||||
const rootFolder =
|
const rootFolder =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.12.0",
|
"version": "0.11.1",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
132
scripts/init.js
132
scripts/init.js
@@ -335,11 +335,36 @@ async function initializeProject(options = {}) {
|
|||||||
console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
|
console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
|
||||||
console.log('Full options object:', JSON.stringify(options));
|
console.log('Full options object:', JSON.stringify(options));
|
||||||
console.log('options.yes:', options.yes);
|
console.log('options.yes:', options.yes);
|
||||||
|
console.log('options.name:', options.name);
|
||||||
console.log('==================================================');
|
console.log('==================================================');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to get project name from package.json if not provided
|
||||||
|
if (!options.name) {
|
||||||
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(packageJsonPath, 'utf8')
|
||||||
|
);
|
||||||
|
if (packageJson.name) {
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
`Found project name '${packageJson.name}' in package.json`
|
||||||
|
);
|
||||||
|
options.name = packageJson.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(
|
||||||
|
'debug',
|
||||||
|
`Could not read project name from package.json: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if we should skip prompts based on the passed options
|
// Determine if we should skip prompts based on the passed options
|
||||||
const skipPrompts = options.yes;
|
const skipPrompts = options.yes || (options.name && options.description);
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
console.log('Skip prompts determined:', skipPrompts);
|
console.log('Skip prompts determined:', skipPrompts);
|
||||||
}
|
}
|
||||||
@@ -349,24 +374,44 @@ async function initializeProject(options = {}) {
|
|||||||
console.log('SKIPPING PROMPTS - Using defaults or provided values');
|
console.log('SKIPPING PROMPTS - Using defaults or provided values');
|
||||||
}
|
}
|
||||||
|
|
||||||
// We no longer need these variables
|
// Use provided options or defaults
|
||||||
|
const projectName = options.name || 'task-master-project';
|
||||||
|
const projectDescription =
|
||||||
|
options.description || 'A project managed with Task Master AI';
|
||||||
|
const projectVersion = options.version || '0.1.0'; // Default from commands.js or here
|
||||||
|
const authorName = options.author || 'Vibe coder'; // Default if not provided
|
||||||
const dryRun = options.dryRun || false;
|
const dryRun = options.dryRun || false;
|
||||||
const addAliases = options.aliases || false;
|
const addAliases = options.aliases || false;
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
log('info', 'DRY RUN MODE: No files will be modified');
|
log('info', 'DRY RUN MODE: No files will be modified');
|
||||||
log('info', 'Would initialize Task Master project');
|
log(
|
||||||
|
'info',
|
||||||
|
`Would initialize project: ${projectName} (${projectVersion})`
|
||||||
|
);
|
||||||
|
log('info', `Description: ${projectDescription}`);
|
||||||
|
log('info', `Author: ${authorName || 'Not specified'}`);
|
||||||
log('info', 'Would create/update necessary project files');
|
log('info', 'Would create/update necessary project files');
|
||||||
if (addAliases) {
|
if (addAliases) {
|
||||||
log('info', 'Would add shell aliases for task-master');
|
log('info', 'Would add shell aliases for task-master');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
dryRun: true
|
dryRun: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create structure using only necessary values
|
// Create structure using determined values
|
||||||
createProjectStructure(addAliases);
|
createProjectStructure(
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
|
addAliases
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Prompting logic (only runs if skipPrompts is false)
|
// Prompting logic (only runs if skipPrompts is false)
|
||||||
log('info', 'Required options not provided, proceeding with prompts.');
|
log('info', 'Required options not provided, proceeding with prompts.');
|
||||||
@@ -376,17 +421,41 @@ async function initializeProject(options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Only prompt for shell aliases
|
// Prompt user for input...
|
||||||
|
const projectName = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan('Enter project name: ')
|
||||||
|
);
|
||||||
|
const projectDescription = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan('Enter project description: ')
|
||||||
|
);
|
||||||
|
const projectVersionInput = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan('Enter project version (default: 1.0.0): ')
|
||||||
|
); // Use a default for prompt
|
||||||
|
const authorName = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan('Enter your name: ')
|
||||||
|
);
|
||||||
const addAliasesInput = await promptQuestion(
|
const addAliasesInput = await promptQuestion(
|
||||||
rl,
|
rl,
|
||||||
chalk.cyan(
|
chalk.cyan('Add shell aliases for task-master? (Y/n): ')
|
||||||
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
|
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
|
||||||
|
const projectVersion = projectVersionInput.trim()
|
||||||
|
? projectVersionInput
|
||||||
|
: '1.0.0';
|
||||||
|
|
||||||
// Confirm settings...
|
// Confirm settings...
|
||||||
console.log('\nTask Master Project settings:');
|
console.log('\nProject settings:');
|
||||||
|
console.log(chalk.blue('Name:'), chalk.white(projectName));
|
||||||
|
console.log(chalk.blue('Description:'), chalk.white(projectDescription));
|
||||||
|
console.log(chalk.blue('Version:'), chalk.white(projectVersion));
|
||||||
|
console.log(
|
||||||
|
chalk.blue('Author:'),
|
||||||
|
chalk.white(authorName || 'Not specified')
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
chalk.blue(
|
chalk.blue(
|
||||||
'Add shell aliases (so you can use "tm" instead of "task-master"):'
|
'Add shell aliases (so you can use "tm" instead of "task-master"):'
|
||||||
@@ -412,18 +481,33 @@ async function initializeProject(options = {}) {
|
|||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
log('info', 'DRY RUN MODE: No files will be modified');
|
log('info', 'DRY RUN MODE: No files will be modified');
|
||||||
log('info', 'Would initialize Task Master project');
|
log(
|
||||||
|
'info',
|
||||||
|
`Would initialize project: ${projectName} (${projectVersion})`
|
||||||
|
);
|
||||||
|
log('info', `Description: ${projectDescription}`);
|
||||||
|
log('info', `Author: ${authorName || 'Not specified'}`);
|
||||||
log('info', 'Would create/update necessary project files');
|
log('info', 'Would create/update necessary project files');
|
||||||
if (addAliasesPrompted) {
|
if (addAliasesPrompted) {
|
||||||
log('info', 'Would add shell aliases for task-master');
|
log('info', 'Would add shell aliases for task-master');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
dryRun: true
|
dryRun: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create structure using only necessary values
|
// Create structure using prompted values, respecting initial options where relevant
|
||||||
createProjectStructure(addAliasesPrompted);
|
createProjectStructure(
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
|
addAliasesPrompted // Use value from prompt
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
rl.close();
|
rl.close();
|
||||||
log('error', `Error during prompting: ${error.message}`); // Use log function
|
log('error', `Error during prompting: ${error.message}`); // Use log function
|
||||||
@@ -442,7 +526,13 @@ function promptQuestion(rl, question) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to create the project structure
|
// Function to create the project structure
|
||||||
function createProjectStructure(addAliases) {
|
function createProjectStructure(
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
|
addAliases
|
||||||
|
) {
|
||||||
const targetDir = process.cwd();
|
const targetDir = process.cwd();
|
||||||
log('info', `Initializing project in ${targetDir}`);
|
log('info', `Initializing project in ${targetDir}`);
|
||||||
|
|
||||||
@@ -452,10 +542,14 @@ function createProjectStructure(addAliases) {
|
|||||||
ensureDirectoryExists(path.join(targetDir, 'tasks'));
|
ensureDirectoryExists(path.join(targetDir, 'tasks'));
|
||||||
|
|
||||||
// Setup MCP configuration for integration with Cursor
|
// Setup MCP configuration for integration with Cursor
|
||||||
setupMCPConfiguration(targetDir);
|
setupMCPConfiguration(targetDir, projectName);
|
||||||
|
|
||||||
// Copy template files with replacements
|
// Copy template files with replacements
|
||||||
const replacements = {
|
const replacements = {
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
projectVersion,
|
||||||
|
authorName,
|
||||||
year: new Date().getFullYear()
|
year: new Date().getFullYear()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -601,7 +695,7 @@ function createProjectStructure(addAliases) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to setup MCP configuration for Cursor integration
|
// Function to setup MCP configuration for Cursor integration
|
||||||
function setupMCPConfiguration(targetDir) {
|
function setupMCPConfiguration(targetDir, projectName) {
|
||||||
const mcpDirPath = path.join(targetDir, '.cursor');
|
const mcpDirPath = path.join(targetDir, '.cursor');
|
||||||
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
|
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
|
||||||
|
|
||||||
@@ -620,9 +714,9 @@ function setupMCPConfiguration(targetDir) {
|
|||||||
PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY',
|
PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY',
|
||||||
MODEL: 'claude-3-7-sonnet-20250219',
|
MODEL: 'claude-3-7-sonnet-20250219',
|
||||||
PERPLEXITY_MODEL: 'sonar-pro',
|
PERPLEXITY_MODEL: 'sonar-pro',
|
||||||
MAX_TOKENS: '64000',
|
MAX_TOKENS: 64000,
|
||||||
TEMPERATURE: '0.2',
|
TEMPERATURE: 0.2,
|
||||||
DEFAULT_SUBTASKS: '5',
|
DEFAULT_SUBTASKS: 5,
|
||||||
DEFAULT_PRIORITY: 'medium'
|
DEFAULT_PRIORITY: 'medium'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,21 +164,10 @@ async function callClaude(
|
|||||||
log('info', 'Calling Claude...');
|
log('info', 'Calling Claude...');
|
||||||
|
|
||||||
// Build the system prompt
|
// Build the system prompt
|
||||||
const systemPrompt = `You are an AI assistant tasked with breaking down a Product Requirements Document (PRD) into a set of sequential development tasks. Your goal is to create exactly <num_tasks>${numTasks}</num_tasks> well-structured, actionable development tasks based on the PRD provided.
|
const systemPrompt = `You are an AI assistant helping to break down a Product Requirements Document (PRD) into a set of sequential development tasks.
|
||||||
|
Your goal is to create ${numTasks} well-structured, actionable development tasks based on the PRD provided.
|
||||||
First, carefully read and analyze the attached PRD
|
|
||||||
|
|
||||||
Before creating the task list, work through the following steps inside <prd_breakdown> tags in your thinking block:
|
|
||||||
|
|
||||||
1. List the key components of the PRD
|
|
||||||
2. Identify the main features and functionalities described
|
|
||||||
3. Note any specific technical requirements or constraints mentioned
|
|
||||||
4. Outline a high-level sequence of tasks that would be needed to implement the PRD
|
|
||||||
|
|
||||||
Consider dependencies, maintainability, and the fact that you don't have access to any existing codebase. Balance between providing detailed task descriptions and maintaining a high-level perspective.
|
|
||||||
|
|
||||||
After your breakdown, create a JSON object containing an array of tasks and a metadata object. Each task should follow this structure:
|
|
||||||
|
|
||||||
|
Each task should follow this JSON structure:
|
||||||
{
|
{
|
||||||
"id": number,
|
"id": number,
|
||||||
"title": string,
|
"title": string,
|
||||||
@@ -190,46 +179,39 @@ After your breakdown, create a JSON object containing an array of tasks and a me
|
|||||||
"testStrategy": string (validation approach)
|
"testStrategy": string (validation approach)
|
||||||
}
|
}
|
||||||
|
|
||||||
Guidelines for creating tasks:
|
Guidelines:
|
||||||
1. Number tasks from 1 to <num_tasks>${numTasks}</num_tasks>.
|
1. Create exactly ${numTasks} tasks, numbered from 1 to ${numTasks}
|
||||||
2. Make each task atomic and focused on a single responsibility.
|
2. Each task should be atomic and focused on a single responsibility
|
||||||
3. Order tasks logically, considering dependencies and implementation sequence.
|
3. Order tasks logically - consider dependencies and implementation sequence
|
||||||
4. Start with setup and core functionality, then move to advanced features.
|
4. Early tasks should focus on setup, core functionality first, then advanced features
|
||||||
5. Provide a clear validation/testing approach for each task.
|
5. Include clear validation/testing approach for each task
|
||||||
6. Set appropriate dependency IDs (tasks can only depend on lower-numbered tasks).
|
6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs)
|
||||||
7. Assign priority based on criticality and dependency order.
|
7. Assign priority (high/medium/low) based on criticality and dependency order
|
||||||
8. Include detailed implementation guidance in the "details" field.
|
8. Include detailed implementation guidance in the "details" field
|
||||||
9. Strictly adhere to any specific requirements for libraries, database schemas, frameworks, tech stacks, or other implementation details mentioned in the PRD.
|
9. If the PRD contains specific requirements for libraries, database schemas, frameworks, tech stacks, or any other implementation details, STRICTLY ADHERE to these requirements in your task breakdown and do not discard them under any circumstance
|
||||||
10. Fill in gaps left by the PRD while preserving all explicit requirements.
|
10. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements
|
||||||
11. Provide the most direct path to implementation, avoiding over-engineering.
|
11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches
|
||||||
|
|
||||||
The final output should be valid JSON with this structure:
|
|
||||||
|
|
||||||
|
Expected output format:
|
||||||
{
|
{
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"title": "Example Task Title",
|
"title": "Setup Project Repository",
|
||||||
"description": "Brief description of the task",
|
"description": "...",
|
||||||
"status": "pending",
|
...
|
||||||
"dependencies": [0],
|
|
||||||
"priority": "high",
|
|
||||||
"details": "Detailed implementation guidance",
|
|
||||||
"testStrategy": "Approach for validating this task"
|
|
||||||
},
|
},
|
||||||
// ... more tasks ...
|
...
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"projectName": "PRD Implementation",
|
"projectName": "PRD Implementation",
|
||||||
"totalTasks": <num_tasks>${numTasks}</num_tasks>,
|
"totalTasks": ${numTasks},
|
||||||
"sourceFile": "<prd_path>${prdPath}</prd_path>",
|
"sourceFile": "${prdPath}",
|
||||||
"generatedAt": "YYYY-MM-DD"
|
"generatedAt": "YYYY-MM-DD"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Remember to provide comprehensive task details that are LLM-friendly, consider dependencies and maintainability carefully, and keep in mind that you don't have the existing codebase as context. Aim for a balance between detailed guidance and high-level planning.
|
Important: Your response must be valid JSON only, with no additional explanation or comments.`;
|
||||||
|
|
||||||
Your response should be valid JSON only, with no additional explanation or comments. Do not duplicate or rehash any of the work you did in the prd_breakdown section in your final output.`;
|
|
||||||
|
|
||||||
// Use streaming request to handle large responses and show progress
|
// Use streaming request to handle large responses and show progress
|
||||||
return await handleStreamingRequest(
|
return await handleStreamingRequest(
|
||||||
|
|||||||
@@ -88,10 +88,6 @@ function registerCommands(programInstance) {
|
|||||||
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
||||||
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
||||||
.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
|
.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
|
||||||
.option(
|
|
||||||
'--append',
|
|
||||||
'Append new tasks to existing tasks.json instead of overwriting'
|
|
||||||
)
|
|
||||||
.action(async (file, options) => {
|
.action(async (file, options) => {
|
||||||
// Use input option if file argument not provided
|
// Use input option if file argument not provided
|
||||||
const inputFile = file || options.input;
|
const inputFile = file || options.input;
|
||||||
@@ -99,11 +95,10 @@ function registerCommands(programInstance) {
|
|||||||
const numTasks = parseInt(options.numTasks, 10);
|
const numTasks = parseInt(options.numTasks, 10);
|
||||||
const outputPath = options.output;
|
const outputPath = options.output;
|
||||||
const force = options.force || false;
|
const force = options.force || false;
|
||||||
const append = options.append || false;
|
|
||||||
|
|
||||||
// Helper function to check if tasks.json exists and confirm overwrite
|
// Helper function to check if tasks.json exists and confirm overwrite
|
||||||
async function confirmOverwriteIfNeeded() {
|
async function confirmOverwriteIfNeeded() {
|
||||||
if (fs.existsSync(outputPath) && !force && !append) {
|
if (fs.existsSync(outputPath) && !force) {
|
||||||
const shouldContinue = await confirmTaskOverwrite(outputPath);
|
const shouldContinue = await confirmTaskOverwrite(outputPath);
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
console.log(chalk.yellow('Operation cancelled by user.'));
|
console.log(chalk.yellow('Operation cancelled by user.'));
|
||||||
@@ -122,7 +117,7 @@ function registerCommands(programInstance) {
|
|||||||
if (!(await confirmOverwriteIfNeeded())) return;
|
if (!(await confirmOverwriteIfNeeded())) return;
|
||||||
|
|
||||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||||
await parsePRD(defaultPrdPath, outputPath, numTasks, { append });
|
await parsePRD(defaultPrdPath, outputPath, numTasks);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,21 +138,17 @@ function registerCommands(programInstance) {
|
|||||||
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
|
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
|
||||||
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
|
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
|
||||||
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
|
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
|
||||||
' -f, --force Skip confirmation when overwriting existing tasks\n' +
|
' -f, --force Skip confirmation when overwriting existing tasks\n\n' +
|
||||||
' --append Append new tasks to existing tasks.json instead of overwriting\n\n' +
|
|
||||||
chalk.cyan('Example:') +
|
chalk.cyan('Example:') +
|
||||||
'\n' +
|
'\n' +
|
||||||
' task-master parse-prd requirements.txt --num-tasks 15\n' +
|
' task-master parse-prd requirements.txt --num-tasks 15\n' +
|
||||||
' task-master parse-prd --input=requirements.txt\n' +
|
' task-master parse-prd --input=requirements.txt\n' +
|
||||||
' task-master parse-prd --force\n' +
|
' task-master parse-prd --force\n\n' +
|
||||||
' task-master parse-prd requirements_v2.txt --append\n\n' +
|
|
||||||
chalk.yellow('Note: This command will:') +
|
chalk.yellow('Note: This command will:') +
|
||||||
'\n' +
|
'\n' +
|
||||||
' 1. Look for a PRD file at scripts/prd.txt by default\n' +
|
' 1. Look for a PRD file at scripts/prd.txt by default\n' +
|
||||||
' 2. Use the file specified by --input or positional argument if provided\n' +
|
' 2. Use the file specified by --input or positional argument if provided\n' +
|
||||||
' 3. Generate tasks from the PRD and either:\n' +
|
' 3. Generate tasks from the PRD and overwrite any existing tasks.json file',
|
||||||
' - Overwrite any existing tasks.json file (default)\n' +
|
|
||||||
' - Append to existing tasks.json if --append is used',
|
|
||||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -169,11 +160,8 @@ function registerCommands(programInstance) {
|
|||||||
|
|
||||||
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
||||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||||
if (append) {
|
|
||||||
console.log(chalk.blue('Appending to existing tasks...'));
|
|
||||||
}
|
|
||||||
|
|
||||||
await parsePRD(inputFile, outputPath, numTasks, { append });
|
await parsePRD(inputFile, outputPath, numTasks);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update command
|
// update command
|
||||||
@@ -1386,18 +1374,18 @@ function registerCommands(programInstance) {
|
|||||||
// remove-task command
|
// remove-task command
|
||||||
programInstance
|
programInstance
|
||||||
.command('remove-task')
|
.command('remove-task')
|
||||||
.description('Remove one or more tasks or subtasks permanently')
|
.description('Remove a task or subtask permanently')
|
||||||
.option(
|
.option(
|
||||||
'-i, --id <id>',
|
'-i, --id <id>',
|
||||||
'ID(s) of the task(s) or subtask(s) to remove (e.g., "5" or "5.2" or "5,6,7")'
|
'ID of the task or subtask to remove (e.g., "5" or "5.2")'
|
||||||
)
|
)
|
||||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||||
.option('-y, --yes', 'Skip confirmation prompt', false)
|
.option('-y, --yes', 'Skip confirmation prompt', false)
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
const tasksPath = options.file;
|
const tasksPath = options.file;
|
||||||
const taskIds = options.id;
|
const taskId = options.id;
|
||||||
|
|
||||||
if (!taskIds) {
|
if (!taskId) {
|
||||||
console.error(chalk.red('Error: Task ID is required'));
|
console.error(chalk.red('Error: Task ID is required'));
|
||||||
console.error(
|
console.error(
|
||||||
chalk.yellow('Usage: task-master remove-task --id=<taskId>')
|
chalk.yellow('Usage: task-master remove-task --id=<taskId>')
|
||||||
@@ -1406,7 +1394,7 @@ function registerCommands(programInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the tasks file exists and is valid
|
// Check if the task exists
|
||||||
const data = readJSON(tasksPath);
|
const data = readJSON(tasksPath);
|
||||||
if (!data || !data.tasks) {
|
if (!data || !data.tasks) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -1415,89 +1403,75 @@ function registerCommands(programInstance) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split task IDs if comma-separated
|
if (!taskExists(data.tasks, taskId)) {
|
||||||
const taskIdArray = taskIds.split(',').map((id) => id.trim());
|
console.error(chalk.red(`Error: Task with ID ${taskId} not found`));
|
||||||
|
|
||||||
// Validate all task IDs exist before proceeding
|
|
||||||
const invalidTasks = taskIdArray.filter(
|
|
||||||
(id) => !taskExists(data.tasks, id)
|
|
||||||
);
|
|
||||||
if (invalidTasks.length > 0) {
|
|
||||||
console.error(
|
|
||||||
chalk.red(
|
|
||||||
`Error: The following tasks were not found: ${invalidTasks.join(', ')}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load task for display
|
||||||
|
const task = findTaskById(data.tasks, taskId);
|
||||||
|
|
||||||
// Skip confirmation if --yes flag is provided
|
// Skip confirmation if --yes flag is provided
|
||||||
if (!options.yes) {
|
if (!options.yes) {
|
||||||
// Display tasks to be removed
|
// Display task information
|
||||||
console.log();
|
console.log();
|
||||||
console.log(
|
console.log(
|
||||||
chalk.red.bold(
|
chalk.red.bold(
|
||||||
'⚠️ WARNING: This will permanently delete the following tasks:'
|
'⚠️ WARNING: This will permanently delete the following task:'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
for (const taskId of taskIdArray) {
|
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||||
const task = findTaskById(data.tasks, taskId);
|
// It's a subtask
|
||||||
|
const [parentId, subtaskId] = taskId.split('.');
|
||||||
|
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
|
||||||
|
console.log(
|
||||||
|
chalk.gray(
|
||||||
|
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// It's a main task
|
||||||
|
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
|
||||||
|
|
||||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
// Show if it has subtasks
|
||||||
// It's a subtask
|
if (task.subtasks && task.subtasks.length > 0) {
|
||||||
const [parentId, subtaskId] = taskId.split('.');
|
|
||||||
console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`));
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.gray(
|
chalk.yellow(
|
||||||
`Parent Task: ${task.parentTask.id} - ${task.parentTask.title}`
|
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// It's a main task
|
|
||||||
console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`));
|
|
||||||
|
|
||||||
// Show if it has subtasks
|
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show if other tasks depend on it
|
|
||||||
const dependentTasks = data.tasks.filter(
|
|
||||||
(t) =>
|
|
||||||
t.dependencies &&
|
|
||||||
t.dependencies.includes(parseInt(taskId, 10))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dependentTasks.length > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
chalk.yellow('These dependencies will be removed:')
|
|
||||||
);
|
|
||||||
dependentTasks.forEach((t) => {
|
|
||||||
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
console.log();
|
|
||||||
|
// Show if other tasks depend on it
|
||||||
|
const dependentTasks = data.tasks.filter(
|
||||||
|
(t) =>
|
||||||
|
t.dependencies && t.dependencies.includes(parseInt(taskId, 10))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dependentTasks.length > 0) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log(chalk.yellow('These dependencies will be removed:'));
|
||||||
|
dependentTasks.forEach((t) => {
|
||||||
|
console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
|
||||||
// Prompt for confirmation
|
// Prompt for confirmation
|
||||||
const { confirm } = await inquirer.prompt([
|
const { confirm } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'confirm',
|
name: 'confirm',
|
||||||
message: chalk.red.bold(
|
message: chalk.red.bold(
|
||||||
`Are you sure you want to permanently delete ${taskIdArray.length > 1 ? 'these tasks' : 'this task'}?`
|
'Are you sure you want to permanently delete this task?'
|
||||||
),
|
),
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
@@ -1509,72 +1483,31 @@ function registerCommands(programInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const indicator = startLoadingIndicator('Removing tasks...');
|
const indicator = startLoadingIndicator('Removing task...');
|
||||||
|
|
||||||
// Remove each task
|
// Remove the task
|
||||||
const results = [];
|
const result = await removeTask(tasksPath, taskId);
|
||||||
for (const taskId of taskIdArray) {
|
|
||||||
try {
|
|
||||||
const result = await removeTask(tasksPath, taskId);
|
|
||||||
results.push({ taskId, success: true, ...result });
|
|
||||||
} catch (error) {
|
|
||||||
results.push({ taskId, success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoadingIndicator(indicator);
|
stopLoadingIndicator(indicator);
|
||||||
|
|
||||||
// Display results
|
// Display success message with appropriate color based on task or subtask
|
||||||
const successfulRemovals = results.filter((r) => r.success);
|
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||||
const failedRemovals = results.filter((r) => !r.success);
|
// It was a subtask
|
||||||
|
|
||||||
if (successfulRemovals.length > 0) {
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.green(
|
chalk.green(`Subtask ${taskId} has been successfully removed`),
|
||||||
`Successfully removed ${successfulRemovals.length} task${successfulRemovals.length > 1 ? 's' : ''}`
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||||
) +
|
|
||||||
'\n\n' +
|
|
||||||
successfulRemovals
|
|
||||||
.map((r) =>
|
|
||||||
chalk.white(
|
|
||||||
`✓ ${r.taskId.includes('.') ? 'Subtask' : 'Task'} ${r.taskId}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.join('\n'),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'green',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1 }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
|
// It was a main task
|
||||||
if (failedRemovals.length > 0) {
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(chalk.green(`Task ${taskId} has been successfully removed`), {
|
||||||
chalk.red(
|
padding: 1,
|
||||||
`Failed to remove ${failedRemovals.length} task${failedRemovals.length > 1 ? 's' : ''}`
|
borderColor: 'green',
|
||||||
) +
|
borderStyle: 'round'
|
||||||
'\n\n' +
|
})
|
||||||
failedRemovals
|
|
||||||
.map((r) => chalk.white(`✗ ${r.taskId}: ${r.error}`))
|
|
||||||
.join('\n'),
|
|
||||||
{
|
|
||||||
padding: 1,
|
|
||||||
borderColor: 'red',
|
|
||||||
borderStyle: 'round',
|
|
||||||
margin: { top: 1 }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Exit with error if any removals failed
|
|
||||||
if (successfulRemovals.length === 0) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async function parsePRD(
|
|||||||
aiClient = null,
|
aiClient = null,
|
||||||
modelConfig = null
|
modelConfig = null
|
||||||
) {
|
) {
|
||||||
const { reportProgress, mcpLog, session, append } = options;
|
const { reportProgress, mcpLog, session } = options;
|
||||||
|
|
||||||
// Determine output format based on mcpLog presence (simplification)
|
// Determine output format based on mcpLog presence (simplification)
|
||||||
const outputFormat = mcpLog ? 'json' : 'text';
|
const outputFormat = mcpLog ? 'json' : 'text';
|
||||||
@@ -127,30 +127,8 @@ async function parsePRD(
|
|||||||
// Read the PRD content
|
// Read the PRD content
|
||||||
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
||||||
|
|
||||||
// If appending and tasks.json exists, read existing tasks first
|
|
||||||
let existingTasks = { tasks: [] };
|
|
||||||
let lastTaskId = 0;
|
|
||||||
if (append && fs.existsSync(tasksPath)) {
|
|
||||||
try {
|
|
||||||
existingTasks = readJSON(tasksPath);
|
|
||||||
if (existingTasks.tasks?.length) {
|
|
||||||
// Find the highest task ID
|
|
||||||
lastTaskId = existingTasks.tasks.reduce((maxId, task) => {
|
|
||||||
const mainId = parseInt(task.id.toString().split('.')[0], 10) || 0;
|
|
||||||
return Math.max(maxId, mainId);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
report(
|
|
||||||
`Warning: Could not read existing tasks file: ${error.message}`,
|
|
||||||
'warn'
|
|
||||||
);
|
|
||||||
existingTasks = { tasks: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Claude to generate tasks, passing the provided AI client if available
|
// Call Claude to generate tasks, passing the provided AI client if available
|
||||||
const newTasksData = await callClaude(
|
const tasksData = await callClaude(
|
||||||
prdContent,
|
prdContent,
|
||||||
prdPath,
|
prdPath,
|
||||||
numTasks,
|
numTasks,
|
||||||
@@ -160,33 +138,15 @@ async function parsePRD(
|
|||||||
modelConfig
|
modelConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update task IDs if appending
|
|
||||||
if (append && lastTaskId > 0) {
|
|
||||||
report(`Updating task IDs to continue from ID ${lastTaskId}`, 'info');
|
|
||||||
newTasksData.tasks.forEach((task, index) => {
|
|
||||||
task.id = lastTaskId + index + 1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge tasks if appending
|
|
||||||
const tasksData = append
|
|
||||||
? {
|
|
||||||
...existingTasks,
|
|
||||||
tasks: [...existingTasks.tasks, ...newTasksData.tasks]
|
|
||||||
}
|
|
||||||
: newTasksData;
|
|
||||||
|
|
||||||
// Create the directory if it doesn't exist
|
// Create the directory if it doesn't exist
|
||||||
const tasksDir = path.dirname(tasksPath);
|
const tasksDir = path.dirname(tasksPath);
|
||||||
if (!fs.existsSync(tasksDir)) {
|
if (!fs.existsSync(tasksDir)) {
|
||||||
fs.mkdirSync(tasksDir, { recursive: true });
|
fs.mkdirSync(tasksDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the tasks to the file
|
// Write the tasks to the file
|
||||||
writeJSON(tasksPath, tasksData);
|
writeJSON(tasksPath, tasksData);
|
||||||
const actionVerb = append ? 'appended' : 'generated';
|
|
||||||
report(
|
report(
|
||||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`,
|
`Successfully generated ${tasksData.tasks.length} tasks from PRD`,
|
||||||
'success'
|
'success'
|
||||||
);
|
);
|
||||||
report(`Tasks saved to: ${tasksPath}`, 'info');
|
report(`Tasks saved to: ${tasksPath}`, 'info');
|
||||||
@@ -206,7 +166,7 @@ async function parsePRD(
|
|||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`
|
`Successfully generated ${tasksData.tasks.length} tasks from PRD`
|
||||||
),
|
),
|
||||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||||
)
|
)
|
||||||
|
|||||||
32
scripts/modules/task-manager.js (lines 3036-3084)
Normal file
32
scripts/modules/task-manager.js (lines 3036-3084)
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) {
|
||||||
|
let loadingIndicator = null;
|
||||||
|
try {
|
||||||
|
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
|
||||||
|
|
||||||
|
// Validate subtask ID format
|
||||||
|
if (!subtaskId || typeof subtaskId !== 'string' || !subtaskId.includes('.')) {
|
||||||
|
throw new Error(`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prompt
|
||||||
|
if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
|
||||||
|
throw new Error('Prompt cannot be empty. Please provide context for the subtask update.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare for fallback handling
|
||||||
|
let claudeOverloaded = false;
|
||||||
|
|
||||||
|
// Validate tasks file exists
|
||||||
|
if (!fs.existsSync(tasksPath)) {
|
||||||
|
throw new Error(`Tasks file not found at path: ${tasksPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the tasks file
|
||||||
|
const data = readJSON(tasksPath);
|
||||||
|
// ... rest of the function
|
||||||
|
} catch (error) {
|
||||||
|
// Handle errors
|
||||||
|
console.error(`Error updating subtask: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
2636
tasks/tasks.json.bak
Normal file
2636
tasks/tasks.json.bak
Normal file
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"subtasks": [
|
"subtasks": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"dependencies": []
|
"dependencies": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -199,35 +199,16 @@ describe('Commands Module', () => {
|
|||||||
// Use input option if file argument not provided
|
// Use input option if file argument not provided
|
||||||
const inputFile = file || options.input;
|
const inputFile = file || options.input;
|
||||||
const defaultPrdPath = 'scripts/prd.txt';
|
const defaultPrdPath = 'scripts/prd.txt';
|
||||||
const append = options.append || false;
|
|
||||||
const force = options.force || false;
|
|
||||||
const outputPath = options.output || 'tasks/tasks.json';
|
|
||||||
|
|
||||||
// Mock confirmOverwriteIfNeeded function to test overwrite behavior
|
|
||||||
const mockConfirmOverwrite = jest.fn().mockResolvedValue(true);
|
|
||||||
|
|
||||||
// Helper function to check if tasks.json exists and confirm overwrite
|
|
||||||
async function confirmOverwriteIfNeeded() {
|
|
||||||
if (fs.existsSync(outputPath) && !force && !append) {
|
|
||||||
return mockConfirmOverwrite();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no input file specified, check for default PRD location
|
// If no input file specified, check for default PRD location
|
||||||
if (!inputFile) {
|
if (!inputFile) {
|
||||||
if (fs.existsSync(defaultPrdPath)) {
|
if (fs.existsSync(defaultPrdPath)) {
|
||||||
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
|
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
|
||||||
const numTasks = parseInt(options.numTasks, 10);
|
const numTasks = parseInt(options.numTasks, 10);
|
||||||
|
const outputPath = options.output;
|
||||||
// Check if we need to confirm overwrite
|
|
||||||
if (!(await confirmOverwriteIfNeeded())) return;
|
|
||||||
|
|
||||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||||
if (append) {
|
await mockParsePRD(defaultPrdPath, outputPath, numTasks);
|
||||||
console.log(chalk.blue('Appending to existing tasks...'));
|
|
||||||
}
|
|
||||||
await mockParsePRD(defaultPrdPath, outputPath, numTasks, { append });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,20 +221,12 @@ describe('Commands Module', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numTasks = parseInt(options.numTasks, 10);
|
const numTasks = parseInt(options.numTasks, 10);
|
||||||
|
const outputPath = options.output;
|
||||||
// Check if we need to confirm overwrite
|
|
||||||
if (!(await confirmOverwriteIfNeeded())) return;
|
|
||||||
|
|
||||||
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
||||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||||
if (append) {
|
|
||||||
console.log(chalk.blue('Appending to existing tasks...'));
|
|
||||||
}
|
|
||||||
|
|
||||||
await mockParsePRD(inputFile, outputPath, numTasks, { append });
|
await mockParsePRD(inputFile, outputPath, numTasks);
|
||||||
|
|
||||||
// Return mock for testing
|
|
||||||
return { mockConfirmOverwrite };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -279,8 +252,7 @@ describe('Commands Module', () => {
|
|||||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||||
'scripts/prd.txt',
|
'scripts/prd.txt',
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
10, // Default value from command definition
|
10 // Default value from command definition
|
||||||
{ append: false }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,8 +290,7 @@ describe('Commands Module', () => {
|
|||||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||||
testFile,
|
testFile,
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
10,
|
10
|
||||||
{ append: false }
|
|
||||||
);
|
);
|
||||||
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
||||||
});
|
});
|
||||||
@@ -342,8 +313,7 @@ describe('Commands Module', () => {
|
|||||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||||
testFile,
|
testFile,
|
||||||
'tasks/tasks.json',
|
'tasks/tasks.json',
|
||||||
10,
|
10
|
||||||
{ append: false }
|
|
||||||
);
|
);
|
||||||
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
||||||
});
|
});
|
||||||
@@ -361,126 +331,7 @@ describe('Commands Module', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, numTasks);
|
||||||
testFile,
|
|
||||||
outputFile,
|
|
||||||
numTasks,
|
|
||||||
{ append: false }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should pass append flag to parsePRD when provided', async () => {
|
|
||||||
// Arrange
|
|
||||||
const testFile = 'test/prd.txt';
|
|
||||||
|
|
||||||
// Act - call the handler directly with append flag
|
|
||||||
await parsePrdAction(testFile, {
|
|
||||||
numTasks: '10',
|
|
||||||
output: 'tasks/tasks.json',
|
|
||||||
append: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('Appending to existing tasks')
|
|
||||||
);
|
|
||||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
|
||||||
testFile,
|
|
||||||
'tasks/tasks.json',
|
|
||||||
10,
|
|
||||||
{ append: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should bypass confirmation when append flag is true and tasks.json exists', async () => {
|
|
||||||
// Arrange
|
|
||||||
const testFile = 'test/prd.txt';
|
|
||||||
const outputFile = 'tasks/tasks.json';
|
|
||||||
|
|
||||||
// Mock that tasks.json exists
|
|
||||||
mockExistsSync.mockImplementation((path) => {
|
|
||||||
if (path === outputFile) return true;
|
|
||||||
if (path === testFile) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act - call the handler with append flag
|
|
||||||
const { mockConfirmOverwrite } =
|
|
||||||
(await parsePrdAction(testFile, {
|
|
||||||
numTasks: '10',
|
|
||||||
output: outputFile,
|
|
||||||
append: true
|
|
||||||
})) || {};
|
|
||||||
|
|
||||||
// Assert - confirm overwrite should not be called with append flag
|
|
||||||
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
|
|
||||||
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
|
|
||||||
append: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset mock implementation
|
|
||||||
mockExistsSync.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should prompt for confirmation when append flag is false and tasks.json exists', async () => {
|
|
||||||
// Arrange
|
|
||||||
const testFile = 'test/prd.txt';
|
|
||||||
const outputFile = 'tasks/tasks.json';
|
|
||||||
|
|
||||||
// Mock that tasks.json exists
|
|
||||||
mockExistsSync.mockImplementation((path) => {
|
|
||||||
if (path === outputFile) return true;
|
|
||||||
if (path === testFile) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act - call the handler without append flag
|
|
||||||
const { mockConfirmOverwrite } =
|
|
||||||
(await parsePrdAction(testFile, {
|
|
||||||
numTasks: '10',
|
|
||||||
output: outputFile
|
|
||||||
// append: false (default)
|
|
||||||
})) || {};
|
|
||||||
|
|
||||||
// Assert - confirm overwrite should be called without append flag
|
|
||||||
expect(mockConfirmOverwrite).toHaveBeenCalled();
|
|
||||||
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
|
|
||||||
append: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset mock implementation
|
|
||||||
mockExistsSync.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should bypass confirmation when force flag is true, regardless of append flag', async () => {
|
|
||||||
// Arrange
|
|
||||||
const testFile = 'test/prd.txt';
|
|
||||||
const outputFile = 'tasks/tasks.json';
|
|
||||||
|
|
||||||
// Mock that tasks.json exists
|
|
||||||
mockExistsSync.mockImplementation((path) => {
|
|
||||||
if (path === outputFile) return true;
|
|
||||||
if (path === testFile) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act - call the handler with force flag
|
|
||||||
const { mockConfirmOverwrite } =
|
|
||||||
(await parsePrdAction(testFile, {
|
|
||||||
numTasks: '10',
|
|
||||||
output: outputFile,
|
|
||||||
force: true,
|
|
||||||
append: false
|
|
||||||
})) || {};
|
|
||||||
|
|
||||||
// Assert - confirm overwrite should not be called with force flag
|
|
||||||
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
|
|
||||||
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
|
|
||||||
append: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset mock implementation
|
|
||||||
mockExistsSync.mockReset();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -134,59 +134,33 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create a simplified version of parsePRD for testing
|
// Create a simplified version of parsePRD for testing
|
||||||
const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => {
|
const testParsePRD = async (prdPath, outputPath, numTasks) => {
|
||||||
const { append = false } = options;
|
|
||||||
try {
|
try {
|
||||||
// Handle existing tasks when append flag is true
|
|
||||||
let existingTasks = { tasks: [] };
|
|
||||||
let lastTaskId = 0;
|
|
||||||
|
|
||||||
// Check if the output file already exists
|
// Check if the output file already exists
|
||||||
if (mockExistsSync(outputPath)) {
|
if (mockExistsSync(outputPath)) {
|
||||||
if (append) {
|
const confirmOverwrite = await mockPromptYesNo(
|
||||||
// Simulate reading existing tasks.json
|
`Warning: ${outputPath} already exists. Overwrite?`,
|
||||||
existingTasks = {
|
false
|
||||||
tasks: [
|
);
|
||||||
{ id: 1, title: 'Existing Task 1', status: 'done' },
|
|
||||||
{ id: 2, title: 'Existing Task 2', status: 'pending' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
lastTaskId = 2; // Highest existing ID
|
|
||||||
} else {
|
|
||||||
const confirmOverwrite = await mockPromptYesNo(
|
|
||||||
`Warning: ${outputPath} already exists. Overwrite?`,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmOverwrite) {
|
if (!confirmOverwrite) {
|
||||||
console.log(`Operation cancelled. ${outputPath} was not modified.`);
|
console.log(`Operation cancelled. ${outputPath} was not modified.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const prdContent = mockReadFileSync(prdPath, 'utf8');
|
const prdContent = mockReadFileSync(prdPath, 'utf8');
|
||||||
// Modify mockCallClaude to accept lastTaskId parameter
|
const tasks = await mockCallClaude(prdContent, prdPath, numTasks);
|
||||||
let newTasks = await mockCallClaude(prdContent, prdPath, numTasks);
|
|
||||||
|
|
||||||
// Merge tasks if appending
|
|
||||||
const tasksData = append
|
|
||||||
? {
|
|
||||||
...existingTasks,
|
|
||||||
tasks: [...existingTasks.tasks, ...newTasks.tasks]
|
|
||||||
}
|
|
||||||
: newTasks;
|
|
||||||
|
|
||||||
const dir = mockDirname(outputPath);
|
const dir = mockDirname(outputPath);
|
||||||
|
|
||||||
if (!mockExistsSync(dir)) {
|
if (!mockExistsSync(dir)) {
|
||||||
mockMkdirSync(dir, { recursive: true });
|
mockMkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
mockWriteJSON(outputPath, tasksData);
|
mockWriteJSON(outputPath, tasks);
|
||||||
await mockGenerateTaskFiles(outputPath, dir);
|
await mockGenerateTaskFiles(outputPath, dir);
|
||||||
|
|
||||||
return tasksData;
|
return tasks;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error parsing PRD: ${error.message}`);
|
console.error(`Error parsing PRD: ${error.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -654,27 +628,6 @@ describe('Task Manager Module', () => {
|
|||||||
// Mock the sample PRD content
|
// Mock the sample PRD content
|
||||||
const samplePRDContent = '# Sample PRD for Testing';
|
const samplePRDContent = '# Sample PRD for Testing';
|
||||||
|
|
||||||
// Mock existing tasks for append test
|
|
||||||
const existingTasks = {
|
|
||||||
tasks: [
|
|
||||||
{ id: 1, title: 'Existing Task 1', status: 'done' },
|
|
||||||
{ id: 2, title: 'Existing Task 2', status: 'pending' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock new tasks with continuing IDs for append test
|
|
||||||
const newTasksWithContinuedIds = {
|
|
||||||
tasks: [
|
|
||||||
{ id: 3, title: 'New Task 3' },
|
|
||||||
{ id: 4, title: 'New Task 4' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock merged tasks for append test
|
|
||||||
const mergedTasks = {
|
|
||||||
tasks: [...existingTasks.tasks, ...newTasksWithContinuedIds.tasks]
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -858,66 +811,6 @@ describe('Task Manager Module', () => {
|
|||||||
sampleClaudeResponse
|
sampleClaudeResponse
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should append new tasks when append option is true', async () => {
|
|
||||||
// Setup mocks to simulate tasks.json already exists
|
|
||||||
mockExistsSync.mockImplementation((path) => {
|
|
||||||
if (path === 'tasks/tasks.json') return true; // Output file exists
|
|
||||||
if (path === 'tasks') return true; // Directory exists
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock for reading existing tasks
|
|
||||||
mockReadJSON.mockReturnValue(existingTasks);
|
|
||||||
// mockReadJSON = jest.fn().mockReturnValue(existingTasks);
|
|
||||||
|
|
||||||
// Mock callClaude to return new tasks with continuing IDs
|
|
||||||
mockCallClaude.mockResolvedValueOnce(newTasksWithContinuedIds);
|
|
||||||
|
|
||||||
// Call the function with append option
|
|
||||||
const result = await testParsePRD(
|
|
||||||
'path/to/prd.txt',
|
|
||||||
'tasks/tasks.json',
|
|
||||||
2,
|
|
||||||
{ append: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify prompt was NOT called (no confirmation needed for append)
|
|
||||||
expect(mockPromptYesNo).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Verify the file was written with merged tasks
|
|
||||||
expect(mockWriteJSON).toHaveBeenCalledWith(
|
|
||||||
'tasks/tasks.json',
|
|
||||||
expect.objectContaining({
|
|
||||||
tasks: expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: 1 }),
|
|
||||||
expect.objectContaining({ id: 2 }),
|
|
||||||
expect.objectContaining({ id: 3 }),
|
|
||||||
expect.objectContaining({ id: 4 })
|
|
||||||
])
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify the result contains merged tasks
|
|
||||||
expect(result.tasks.length).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should skip prompt and not overwrite when append is true', async () => {
|
|
||||||
// Setup mocks to simulate tasks.json already exists
|
|
||||||
mockExistsSync.mockImplementation((path) => {
|
|
||||||
if (path === 'tasks/tasks.json') return true; // Output file exists
|
|
||||||
if (path === 'tasks') return true; // Directory exists
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call the function with append option
|
|
||||||
await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
|
|
||||||
append: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify prompt was NOT called with append flag
|
|
||||||
expect(mockPromptYesNo).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip('updateTasks function', () => {
|
describe.skip('updateTasks function', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user