Compare commits
10 Commits
fix/140-de
...
v0.12.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddf0947710 | ||
|
|
3a6bc43778 | ||
|
|
73aa7ac32e | ||
|
|
0300582b46 | ||
|
|
3aee9bc840 | ||
|
|
ff8e75cded | ||
|
|
3e872f8afb | ||
|
|
0eb16d5ecb | ||
|
|
c17d912237 | ||
|
|
41b979c239 |
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'task-master-ai': patch
|
||||
---
|
||||
|
||||
- Fixes shebang issue not allowing task-master to run on certain windows operating systems
|
||||
- Resolves #241 #211 #184 #193
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'task-master-ai': patch
|
||||
---
|
||||
|
||||
Fix remove-task command to handle multiple comma-separated task IDs
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'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.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'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`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'task-master-ai': minor
|
||||
---
|
||||
|
||||
Add `npx task-master-ai` that runs mcp instead of using `task-master-mcp``
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,5 +1,35 @@
|
||||
# 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
|
||||
|
||||
### 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",
|
||||
"MODEL": "claude-3-7-sonnet-20250219",
|
||||
"PERPLEXITY_MODEL": "sonar-pro",
|
||||
"MAX_TOKENS": 64000,
|
||||
"TEMPERATURE": 0.2,
|
||||
"DEFAULT_SUBTASKS": 5,
|
||||
"MAX_TOKENS": "64000",
|
||||
"TEMPERATURE": "0.2",
|
||||
"DEFAULT_SUBTASKS": "5",
|
||||
"DEFAULT_PRIORITY": "medium"
|
||||
}
|
||||
}
|
||||
|
||||
44
index.js
44
index.js
@@ -46,22 +46,18 @@ export const initProject = async (options = {}) => {
|
||||
};
|
||||
|
||||
// Export a function to run init as a CLI command
|
||||
export const runInitCLI = async () => {
|
||||
// Using spawn to ensure proper handling of stdio and process exit
|
||||
const child = spawn('node', [resolve(__dirname, './scripts/init.js')], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Init script exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
export const runInitCLI = async (options = {}) => {
|
||||
try {
|
||||
const init = await import('./scripts/init.js');
|
||||
const result = await init.initializeProject(options);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Initialization failed:', error.message);
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Debug stack trace:', error.stack);
|
||||
}
|
||||
throw error; // Re-throw to be handled by the command handler
|
||||
}
|
||||
};
|
||||
|
||||
// Export version information
|
||||
@@ -79,11 +75,21 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
program
|
||||
.command('init')
|
||||
.description('Initialize a new project')
|
||||
.action(() => {
|
||||
runInitCLI().catch((err) => {
|
||||
.option('-y, --yes', 'Skip prompts and use default values')
|
||||
.option('-n, --name <n>', 'Project name')
|
||||
.option('-d, --description <description>', 'Project description')
|
||||
.option('-v, --version <version>', 'Project version', '0.1.0')
|
||||
.option('-a, --author <author>', 'Author name')
|
||||
.option('--skip-install', 'Skip installing dependencies')
|
||||
.option('--dry-run', 'Show what would be done without making changes')
|
||||
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
||||
.action(async (cmdOptions) => {
|
||||
try {
|
||||
await runInitCLI(cmdOptions);
|
||||
} catch (err) {
|
||||
console.error('Init failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
|
||||
@@ -10,7 +10,7 @@ import os from 'os'; // Import os module for home directory check
|
||||
/**
|
||||
* Direct function wrapper for initializing a project.
|
||||
* Derives target directory from session, sets CWD, and calls core init logic.
|
||||
* @param {object} args - Arguments containing project details and options (projectName, projectDescription, yes, etc.)
|
||||
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot)
|
||||
* @param {object} log - The FastMCP logger instance.
|
||||
* @param {object} context - The context object, must contain { session }.
|
||||
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
||||
@@ -92,12 +92,8 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
||||
try {
|
||||
// Always force yes: true when called via MCP to avoid interactive prompts
|
||||
const options = {
|
||||
name: args.projectName,
|
||||
description: args.projectDescription,
|
||||
version: args.projectVersion,
|
||||
author: args.authorName,
|
||||
skipInstall: args.skipInstall,
|
||||
aliases: args.addAliases,
|
||||
skipInstall: args.skipInstall,
|
||||
yes: true // Force yes mode
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os'; // Import os module for home directory check
|
||||
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
||||
import { findTasksJsonPath } from '../utils/path-utils.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
@@ -124,8 +122,12 @@ 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(
|
||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`
|
||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
|
||||
);
|
||||
|
||||
// Create the logger wrapper for proper logging in the core function
|
||||
@@ -157,7 +159,8 @@ export async function parsePRDDirect(args, log, context = {}) {
|
||||
numTasks,
|
||||
{
|
||||
mcpLog: logWrapper,
|
||||
session
|
||||
session,
|
||||
append
|
||||
},
|
||||
aiClient,
|
||||
modelConfig
|
||||
@@ -167,16 +170,18 @@ export async function parsePRDDirect(args, log, context = {}) {
|
||||
// to return it to the caller
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
log.info(
|
||||
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`
|
||||
);
|
||||
const actionVerb = append ? 'appended' : 'generated';
|
||||
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
|
||||
|
||||
log.info(message);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
|
||||
message,
|
||||
taskCount: tasksData.tasks?.length || 0,
|
||||
outputPath
|
||||
outputPath,
|
||||
appended: append
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
|
||||
@@ -10,32 +10,8 @@ export function registerInitializeProjectTool(server) {
|
||||
server.addTool({
|
||||
name: 'initialize_project',
|
||||
description:
|
||||
"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.",
|
||||
'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.',
|
||||
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
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -47,15 +23,13 @@ export function registerInitializeProjectTool(server) {
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
'Add shell aliases (tm, taskmaster) to shell config file. User input not needed.'
|
||||
),
|
||||
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
|
||||
yes: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.default(true)
|
||||
.describe(
|
||||
"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."
|
||||
'Skip prompts and use default values. Always set to true for MCP tools.'
|
||||
),
|
||||
projectRoot: z
|
||||
.string()
|
||||
|
||||
@@ -47,6 +47,12 @@ export function registerParsePRDTool(server) {
|
||||
.boolean()
|
||||
.optional()
|
||||
.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
|
||||
.string()
|
||||
.describe('The directory of the project. Must be absolute path.')
|
||||
@@ -86,7 +92,8 @@ export function registerParsePRDTool(server) {
|
||||
input: prdPath,
|
||||
output: tasksJsonPath,
|
||||
numTasks: args.numTasks,
|
||||
force: args.force
|
||||
force: args.force,
|
||||
append: args.append
|
||||
},
|
||||
log,
|
||||
{ session }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.11.1",
|
||||
"version": "0.12.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
132
scripts/init.js
132
scripts/init.js
@@ -335,36 +335,11 @@ async function initializeProject(options = {}) {
|
||||
console.log('===== DEBUG: INITIALIZE PROJECT OPTIONS RECEIVED =====');
|
||||
console.log('Full options object:', JSON.stringify(options));
|
||||
console.log('options.yes:', options.yes);
|
||||
console.log('options.name:', options.name);
|
||||
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
|
||||
const skipPrompts = options.yes || (options.name && options.description);
|
||||
const skipPrompts = options.yes;
|
||||
if (!isSilentMode()) {
|
||||
console.log('Skip prompts determined:', skipPrompts);
|
||||
}
|
||||
@@ -374,44 +349,24 @@ async function initializeProject(options = {}) {
|
||||
console.log('SKIPPING PROMPTS - Using defaults or provided values');
|
||||
}
|
||||
|
||||
// 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
|
||||
// We no longer need these variables
|
||||
const dryRun = options.dryRun || false;
|
||||
const addAliases = options.aliases || false;
|
||||
|
||||
if (dryRun) {
|
||||
log('info', 'DRY RUN MODE: No files will be modified');
|
||||
log(
|
||||
'info',
|
||||
`Would initialize project: ${projectName} (${projectVersion})`
|
||||
);
|
||||
log('info', `Description: ${projectDescription}`);
|
||||
log('info', `Author: ${authorName || 'Not specified'}`);
|
||||
log('info', 'Would initialize Task Master project');
|
||||
log('info', 'Would create/update necessary project files');
|
||||
if (addAliases) {
|
||||
log('info', 'Would add shell aliases for task-master');
|
||||
}
|
||||
return {
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
dryRun: true
|
||||
};
|
||||
}
|
||||
|
||||
// Create structure using determined values
|
||||
createProjectStructure(
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
addAliases
|
||||
);
|
||||
// Create structure using only necessary values
|
||||
createProjectStructure(addAliases);
|
||||
} else {
|
||||
// Prompting logic (only runs if skipPrompts is false)
|
||||
log('info', 'Required options not provided, proceeding with prompts.');
|
||||
@@ -421,41 +376,17 @@ async function initializeProject(options = {}) {
|
||||
});
|
||||
|
||||
try {
|
||||
// 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: ')
|
||||
);
|
||||
// Only prompt for shell aliases
|
||||
const addAliasesInput = await promptQuestion(
|
||||
rl,
|
||||
chalk.cyan('Add shell aliases for task-master? (Y/n): ')
|
||||
chalk.cyan(
|
||||
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
|
||||
)
|
||||
);
|
||||
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
|
||||
const projectVersion = projectVersionInput.trim()
|
||||
? projectVersionInput
|
||||
: '1.0.0';
|
||||
|
||||
// Confirm 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('\nTask Master Project settings:');
|
||||
console.log(
|
||||
chalk.blue(
|
||||
'Add shell aliases (so you can use "tm" instead of "task-master"):'
|
||||
@@ -481,33 +412,18 @@ async function initializeProject(options = {}) {
|
||||
|
||||
if (dryRun) {
|
||||
log('info', 'DRY RUN MODE: No files will be modified');
|
||||
log(
|
||||
'info',
|
||||
`Would initialize project: ${projectName} (${projectVersion})`
|
||||
);
|
||||
log('info', `Description: ${projectDescription}`);
|
||||
log('info', `Author: ${authorName || 'Not specified'}`);
|
||||
log('info', 'Would initialize Task Master project');
|
||||
log('info', 'Would create/update necessary project files');
|
||||
if (addAliasesPrompted) {
|
||||
log('info', 'Would add shell aliases for task-master');
|
||||
}
|
||||
return {
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
dryRun: true
|
||||
};
|
||||
}
|
||||
|
||||
// Create structure using prompted values, respecting initial options where relevant
|
||||
createProjectStructure(
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
addAliasesPrompted // Use value from prompt
|
||||
);
|
||||
// Create structure using only necessary values
|
||||
createProjectStructure(addAliasesPrompted);
|
||||
} catch (error) {
|
||||
rl.close();
|
||||
log('error', `Error during prompting: ${error.message}`); // Use log function
|
||||
@@ -526,13 +442,7 @@ function promptQuestion(rl, question) {
|
||||
}
|
||||
|
||||
// Function to create the project structure
|
||||
function createProjectStructure(
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
addAliases
|
||||
) {
|
||||
function createProjectStructure(addAliases) {
|
||||
const targetDir = process.cwd();
|
||||
log('info', `Initializing project in ${targetDir}`);
|
||||
|
||||
@@ -542,14 +452,10 @@ function createProjectStructure(
|
||||
ensureDirectoryExists(path.join(targetDir, 'tasks'));
|
||||
|
||||
// Setup MCP configuration for integration with Cursor
|
||||
setupMCPConfiguration(targetDir, projectName);
|
||||
setupMCPConfiguration(targetDir);
|
||||
|
||||
// Copy template files with replacements
|
||||
const replacements = {
|
||||
projectName,
|
||||
projectDescription,
|
||||
projectVersion,
|
||||
authorName,
|
||||
year: new Date().getFullYear()
|
||||
};
|
||||
|
||||
@@ -695,7 +601,7 @@ function createProjectStructure(
|
||||
}
|
||||
|
||||
// Function to setup MCP configuration for Cursor integration
|
||||
function setupMCPConfiguration(targetDir, projectName) {
|
||||
function setupMCPConfiguration(targetDir) {
|
||||
const mcpDirPath = path.join(targetDir, '.cursor');
|
||||
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
|
||||
|
||||
@@ -714,9 +620,9 @@ function setupMCPConfiguration(targetDir, projectName) {
|
||||
PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY',
|
||||
MODEL: 'claude-3-7-sonnet-20250219',
|
||||
PERPLEXITY_MODEL: 'sonar-pro',
|
||||
MAX_TOKENS: 64000,
|
||||
TEMPERATURE: 0.2,
|
||||
DEFAULT_SUBTASKS: 5,
|
||||
MAX_TOKENS: '64000',
|
||||
TEMPERATURE: '0.2',
|
||||
DEFAULT_SUBTASKS: '5',
|
||||
DEFAULT_PRIORITY: 'medium'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,10 +164,21 @@ async function callClaude(
|
||||
log('info', 'Calling Claude...');
|
||||
|
||||
// Build the system prompt
|
||||
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.
|
||||
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.
|
||||
|
||||
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,
|
||||
"title": string,
|
||||
@@ -179,39 +190,46 @@ Each task should follow this JSON structure:
|
||||
"testStrategy": string (validation approach)
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
1. Create exactly ${numTasks} tasks, numbered from 1 to ${numTasks}
|
||||
2. Each task should be atomic and focused on a single responsibility
|
||||
3. Order tasks logically - consider dependencies and implementation sequence
|
||||
4. Early tasks should focus on setup, core functionality first, then advanced features
|
||||
5. Include clear validation/testing approach for each task
|
||||
6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs)
|
||||
7. Assign priority (high/medium/low) based on criticality and dependency order
|
||||
8. Include detailed implementation guidance in the "details" field
|
||||
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. Focus on filling in any gaps left by the PRD or areas that aren't fully specified, while preserving all explicit requirements
|
||||
11. Always aim to provide the most direct path to implementation, avoiding over-engineering or roundabout approaches
|
||||
Guidelines for creating tasks:
|
||||
1. Number tasks from 1 to <num_tasks>${numTasks}</num_tasks>.
|
||||
2. Make each task atomic and focused on a single responsibility.
|
||||
3. Order tasks logically, considering dependencies and implementation sequence.
|
||||
4. Start with setup and core functionality, then move to advanced features.
|
||||
5. Provide a clear validation/testing approach for each task.
|
||||
6. Set appropriate dependency IDs (tasks can only depend on lower-numbered tasks).
|
||||
7. Assign priority based on criticality and dependency order.
|
||||
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.
|
||||
10. Fill in gaps left by the PRD while preserving all explicit requirements.
|
||||
11. Provide the most direct path to implementation, avoiding over-engineering.
|
||||
|
||||
The final output should be valid JSON with this structure:
|
||||
|
||||
Expected output format:
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Setup Project Repository",
|
||||
"description": "...",
|
||||
...
|
||||
"title": "Example Task Title",
|
||||
"description": "Brief description of the task",
|
||||
"status": "pending",
|
||||
"dependencies": [0],
|
||||
"priority": "high",
|
||||
"details": "Detailed implementation guidance",
|
||||
"testStrategy": "Approach for validating this task"
|
||||
},
|
||||
...
|
||||
// ... more tasks ...
|
||||
],
|
||||
"metadata": {
|
||||
"projectName": "PRD Implementation",
|
||||
"totalTasks": ${numTasks},
|
||||
"sourceFile": "${prdPath}",
|
||||
"totalTasks": <num_tasks>${numTasks}</num_tasks>,
|
||||
"sourceFile": "<prd_path>${prdPath}</prd_path>",
|
||||
"generatedAt": "YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
|
||||
Important: Your response must be valid JSON only, with no additional explanation or comments.`;
|
||||
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.
|
||||
|
||||
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
|
||||
return await handleStreamingRequest(
|
||||
|
||||
@@ -88,6 +88,10 @@ function registerCommands(programInstance) {
|
||||
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
||||
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
||||
.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) => {
|
||||
// Use input option if file argument not provided
|
||||
const inputFile = file || options.input;
|
||||
@@ -95,10 +99,11 @@ function registerCommands(programInstance) {
|
||||
const numTasks = parseInt(options.numTasks, 10);
|
||||
const outputPath = options.output;
|
||||
const force = options.force || false;
|
||||
const append = options.append || false;
|
||||
|
||||
// Helper function to check if tasks.json exists and confirm overwrite
|
||||
async function confirmOverwriteIfNeeded() {
|
||||
if (fs.existsSync(outputPath) && !force) {
|
||||
if (fs.existsSync(outputPath) && !force && !append) {
|
||||
const shouldContinue = await confirmTaskOverwrite(outputPath);
|
||||
if (!shouldContinue) {
|
||||
console.log(chalk.yellow('Operation cancelled by user.'));
|
||||
@@ -117,7 +122,7 @@ function registerCommands(programInstance) {
|
||||
if (!(await confirmOverwriteIfNeeded())) return;
|
||||
|
||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||
await parsePRD(defaultPrdPath, outputPath, numTasks);
|
||||
await parsePRD(defaultPrdPath, outputPath, numTasks, { append });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,17 +143,21 @@ function registerCommands(programInstance) {
|
||||
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
|
||||
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
|
||||
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
|
||||
' -f, --force Skip confirmation when overwriting existing tasks\n\n' +
|
||||
' -f, --force Skip confirmation when overwriting existing tasks\n' +
|
||||
' --append Append new tasks to existing tasks.json instead of overwriting\n\n' +
|
||||
chalk.cyan('Example:') +
|
||||
'\n' +
|
||||
' task-master parse-prd requirements.txt --num-tasks 15\n' +
|
||||
' task-master parse-prd --input=requirements.txt\n' +
|
||||
' task-master parse-prd --force\n\n' +
|
||||
' task-master parse-prd --force\n' +
|
||||
' task-master parse-prd requirements_v2.txt --append\n\n' +
|
||||
chalk.yellow('Note: This command will:') +
|
||||
'\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' +
|
||||
' 3. Generate tasks from the PRD and overwrite any existing tasks.json file',
|
||||
' 3. Generate tasks from the PRD and either:\n' +
|
||||
' - Overwrite any existing tasks.json file (default)\n' +
|
||||
' - Append to existing tasks.json if --append is used',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
@@ -160,8 +169,11 @@ function registerCommands(programInstance) {
|
||||
|
||||
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||
if (append) {
|
||||
console.log(chalk.blue('Appending to existing tasks...'));
|
||||
}
|
||||
|
||||
await parsePRD(inputFile, outputPath, numTasks);
|
||||
await parsePRD(inputFile, outputPath, numTasks, { append });
|
||||
});
|
||||
|
||||
// update command
|
||||
|
||||
@@ -106,7 +106,7 @@ async function parsePRD(
|
||||
aiClient = null,
|
||||
modelConfig = null
|
||||
) {
|
||||
const { reportProgress, mcpLog, session } = options;
|
||||
const { reportProgress, mcpLog, session, append } = options;
|
||||
|
||||
// Determine output format based on mcpLog presence (simplification)
|
||||
const outputFormat = mcpLog ? 'json' : 'text';
|
||||
@@ -127,8 +127,30 @@ async function parsePRD(
|
||||
// Read the PRD content
|
||||
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
|
||||
const tasksData = await callClaude(
|
||||
const newTasksData = await callClaude(
|
||||
prdContent,
|
||||
prdPath,
|
||||
numTasks,
|
||||
@@ -138,15 +160,33 @@ async function parsePRD(
|
||||
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
|
||||
const tasksDir = path.dirname(tasksPath);
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the tasks to the file
|
||||
writeJSON(tasksPath, tasksData);
|
||||
const actionVerb = append ? 'appended' : 'generated';
|
||||
report(
|
||||
`Successfully generated ${tasksData.tasks.length} tasks from PRD`,
|
||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`,
|
||||
'success'
|
||||
);
|
||||
report(`Tasks saved to: ${tasksPath}`, 'info');
|
||||
@@ -166,7 +206,7 @@ async function parsePRD(
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(
|
||||
`Successfully generated ${tasksData.tasks.length} tasks from PRD`
|
||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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
2636
tasks/tasks.json.bak
File diff suppressed because one or more lines are too long
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": [],
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": [],
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -199,16 +199,35 @@ describe('Commands Module', () => {
|
||||
// Use input option if file argument not provided
|
||||
const inputFile = file || options.input;
|
||||
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 (!inputFile) {
|
||||
if (fs.existsSync(defaultPrdPath)) {
|
||||
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
|
||||
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...`));
|
||||
await mockParsePRD(defaultPrdPath, outputPath, numTasks);
|
||||
if (append) {
|
||||
console.log(chalk.blue('Appending to existing tasks...'));
|
||||
}
|
||||
await mockParsePRD(defaultPrdPath, outputPath, numTasks, { append });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,12 +240,20 @@ describe('Commands Module', () => {
|
||||
}
|
||||
|
||||
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(`Generating ${numTasks} tasks...`));
|
||||
if (append) {
|
||||
console.log(chalk.blue('Appending to existing tasks...'));
|
||||
}
|
||||
|
||||
await mockParsePRD(inputFile, outputPath, numTasks);
|
||||
await mockParsePRD(inputFile, outputPath, numTasks, { append });
|
||||
|
||||
// Return mock for testing
|
||||
return { mockConfirmOverwrite };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -252,7 +279,8 @@ describe('Commands Module', () => {
|
||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||
'scripts/prd.txt',
|
||||
'tasks/tasks.json',
|
||||
10 // Default value from command definition
|
||||
10, // Default value from command definition
|
||||
{ append: false }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -290,7 +318,8 @@ describe('Commands Module', () => {
|
||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||
testFile,
|
||||
'tasks/tasks.json',
|
||||
10
|
||||
10,
|
||||
{ append: false }
|
||||
);
|
||||
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
||||
});
|
||||
@@ -313,7 +342,8 @@ describe('Commands Module', () => {
|
||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||
testFile,
|
||||
'tasks/tasks.json',
|
||||
10
|
||||
10,
|
||||
{ append: false }
|
||||
);
|
||||
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
|
||||
});
|
||||
@@ -331,7 +361,126 @@ describe('Commands Module', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, numTasks);
|
||||
expect(mockParsePRD).toHaveBeenCalledWith(
|
||||
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,33 +134,59 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
|
||||
});
|
||||
|
||||
// Create a simplified version of parsePRD for testing
|
||||
const testParsePRD = async (prdPath, outputPath, numTasks) => {
|
||||
const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => {
|
||||
const { append = false } = options;
|
||||
try {
|
||||
// Handle existing tasks when append flag is true
|
||||
let existingTasks = { tasks: [] };
|
||||
let lastTaskId = 0;
|
||||
|
||||
// Check if the output file already exists
|
||||
if (mockExistsSync(outputPath)) {
|
||||
const confirmOverwrite = await mockPromptYesNo(
|
||||
`Warning: ${outputPath} already exists. Overwrite?`,
|
||||
false
|
||||
);
|
||||
if (append) {
|
||||
// Simulate reading existing tasks.json
|
||||
existingTasks = {
|
||||
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) {
|
||||
console.log(`Operation cancelled. ${outputPath} was not modified.`);
|
||||
return null;
|
||||
if (!confirmOverwrite) {
|
||||
console.log(`Operation cancelled. ${outputPath} was not modified.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const prdContent = mockReadFileSync(prdPath, 'utf8');
|
||||
const tasks = await mockCallClaude(prdContent, prdPath, numTasks);
|
||||
// Modify mockCallClaude to accept lastTaskId parameter
|
||||
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);
|
||||
|
||||
if (!mockExistsSync(dir)) {
|
||||
mockMkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
mockWriteJSON(outputPath, tasks);
|
||||
mockWriteJSON(outputPath, tasksData);
|
||||
await mockGenerateTaskFiles(outputPath, dir);
|
||||
|
||||
return tasks;
|
||||
return tasksData;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing PRD: ${error.message}`);
|
||||
process.exit(1);
|
||||
@@ -628,6 +654,27 @@ describe('Task Manager Module', () => {
|
||||
// Mock the sample PRD content
|
||||
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(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
@@ -811,6 +858,66 @@ describe('Task Manager Module', () => {
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user