feat: Enhance task management and fix initialization issues

This commit includes several important improvements:

1. Add support for updating multiple tasks at once with comma-separated IDs
   - Modify setTaskStatus to handle lists like id=1,1.1,1.2
   - Fix subtask handling to properly update subtask statuses
   - Add in-progress as a valid status option

2. Fix initialization script issues
   - Add debugging information to help diagnose npx execution problems
   - Improve error handling and readline interface management
   - Remove conditional check that prevented script from running in some environments
   - Add troubleshooting section to README.md

3. Improve package preparation
   - Make scripts executable during package preparation
   - Update version to 1.0.7

These changes make the package more robust and user-friendly, particularly
for first-time users and those managing complex task hierarchies.
This commit is contained in:
Eyal Toledano
2025-03-04 14:46:46 -05:00
parent c72373d761
commit 93bc6b363a
5 changed files with 175 additions and 79 deletions

View File

@@ -29,6 +29,24 @@ This will prompt you for project details and set up a new project with the neces
1. This package uses ES modules. Your package.json should include `"type": "module"`. 1. This package uses ES modules. Your package.json should include `"type": "module"`.
2. The Anthropic SDK version should be 0.39.0 or higher. 2. The Anthropic SDK version should be 0.39.0 or higher.
## Troubleshooting
### If `npx claude-task-init` doesn't respond:
Try running it with Node directly:
```bash
node node_modules/claude-task-master/scripts/init.js
```
Or clone the repository and run:
```bash
git clone https://github.com/eyaltoledano/claude-task-master.git
cd claude-task-master
node scripts/init.js
```
## Integrating with Cursor AI ## Integrating with Cursor AI
Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development. Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development.

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-task-master", "name": "claude-task-master",
"version": "1.0.2", "version": "1.0.7",
"description": "A task management system for AI-driven development with Claude", "description": "A task management system for AI-driven development with Claude",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View File

@@ -15,7 +15,8 @@
* -> Generates per-task files (e.g., task_001.txt) from tasks.json * -> Generates per-task files (e.g., task_001.txt) from tasks.json
* *
* 4) set-status --id=4 --status=done * 4) set-status --id=4 --status=done
* -> Updates a single task's status to done (or pending, deferred, etc.). * -> Updates a single task's status to done (or pending, deferred, in-progress, etc.).
* -> Supports comma-separated IDs for updating multiple tasks: --id=1,2,3,1.1,1.2
* *
* 5) list * 5) list
* -> Lists tasks in a brief console view (ID, title, status). * -> Lists tasks in a brief console view (ID, title, status).
@@ -302,13 +303,65 @@ function generateTaskFiles(tasksPath, outputDir) {
// //
// 4) set-status // 4) set-status
// //
function setTaskStatus(tasksPath, taskId, newStatus) { function setTaskStatus(tasksPath, taskIdInput, newStatus) {
// For recursive calls with multiple IDs, we need to read the latest data each time
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', "No valid tasks found."); log('error', "No valid tasks found.");
process.exit(1); process.exit(1);
} }
// Handle multiple task IDs (comma-separated)
if (typeof taskIdInput === 'string' && taskIdInput.includes(',')) {
const taskIds = taskIdInput.split(',').map(id => id.trim());
log('info', `Processing multiple tasks: ${taskIds.join(', ')}`);
// Process each task ID individually
for (const taskId of taskIds) {
// Create a new instance for each task to ensure we're working with fresh data
setTaskStatus(tasksPath, taskId, newStatus);
}
return;
}
// Convert numeric taskId to number if it's not a subtask ID
const taskId = (!isNaN(taskIdInput) && !String(taskIdInput).includes('.'))
? parseInt(taskIdInput, 10)
: taskIdInput;
// Check if this is a subtask ID (e.g., "1.1")
if (typeof taskId === 'string' && taskId.includes('.')) {
const [parentIdStr, subtaskIdStr] = taskId.split('.');
const parentId = parseInt(parentIdStr, 10);
const subtaskId = parseInt(subtaskIdStr, 10);
const parentTask = data.tasks.find(t => t.id === parentId);
if (!parentTask) {
log('error', `Parent task with ID=${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
log('error', `Parent task with ID=${parentId} has no subtasks.`);
process.exit(1);
}
const subtask = parentTask.subtasks.find(st => st.id === subtaskId);
if (!subtask) {
log('error', `Subtask with ID=${subtaskId} not found in parent task ID=${parentId}.`);
process.exit(1);
}
const oldStatus = subtask.status;
subtask.status = newStatus;
writeJSON(tasksPath, data);
log('info', `Subtask ${parentId}.${subtaskId} status changed from '${oldStatus}' to '${newStatus}'.`);
return;
}
// Handle regular task ID
const task = data.tasks.find(t => t.id === taskId); const task = data.tasks.find(t => t.id === taskId);
if (!task) { if (!task) {
log('error', `Task with ID=${taskId} not found.`); log('error', `Task with ID=${taskId} not found.`);
@@ -701,11 +754,11 @@ function parseSubtasksFromText(text, startId, expectedCount) {
process.exit(1); process.exit(1);
} }
if (!statusArg) { if (!statusArg) {
log('error', "Missing --status=<newStatus> argument (e.g. done, pending, deferred)."); log('error', "Missing --status=<newStatus> argument (e.g., done, pending, deferred, in-progress).");
process.exit(1); process.exit(1);
} }
log('info', `Setting task ${idArg} status to "${statusArg}"...`); log('info', `Setting task(s) ${idArg} status to "${statusArg}"...`);
setTaskStatus(tasksPath, parseInt(idArg, 10), statusArg); setTaskStatus(tasksPath, idArg, statusArg);
break; break;
case 'list': case 'list':
@@ -744,7 +797,8 @@ Subcommands:
-> Generates per-task files (e.g., task_001.txt) from tasks.json -> Generates per-task files (e.g., task_001.txt) from tasks.json
4) set-status --id=4 --status=done 4) set-status --id=4 --status=done
-> Updates a single task's status to done (or pending, deferred, etc.). -> Updates a single task's status to done (or pending, deferred, in-progress, etc.).
-> Supports comma-separated IDs for updating multiple tasks: --id=1,2,3,1.1,1.2
5) list 5) list
-> Lists tasks in a brief console view (ID, title, status). -> Lists tasks in a brief console view (ID, title, status).

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
console.log('Starting claude-task-init...');
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
@@ -7,15 +9,14 @@ import readline from 'readline';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path'; import { dirname } from 'path';
// Debug information
console.log('Node version:', process.version);
console.log('Current directory:', process.cwd());
console.log('Script path:', import.meta.url);
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
// Create readline interface for user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Define log levels and colors // Define log levels and colors
const LOG_LEVELS = { const LOG_LEVELS = {
debug: 0, debug: 0,
@@ -88,8 +89,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
// Main function to initialize a new project // Main function to initialize a new project
async function initializeProject(options = {}) { async function initializeProject(options = {}) {
return new Promise((resolve) => { // If options are provided, use them directly without prompting
// If options are provided, use them directly
if (options.projectName && options.projectDescription) { if (options.projectName && options.projectDescription) {
const projectName = options.projectName; const projectName = options.projectName;
const projectDescription = options.projectDescription; const projectDescription = options.projectDescription;
@@ -97,38 +97,55 @@ async function initializeProject(options = {}) {
const authorName = options.authorName || ''; const authorName = options.authorName || '';
createProjectStructure(projectName, projectDescription, projectVersion, authorName); createProjectStructure(projectName, projectDescription, projectVersion, authorName);
resolve({ return {
projectName, projectName,
projectDescription, projectDescription,
projectVersion, projectVersion,
authorName authorName
}); };
} else {
// Otherwise, prompt the user for input
rl.question('Enter project name: ', (projectName) => {
rl.question('Enter project description: ', (projectDescription) => {
rl.question('Enter project version (default: 1.0.0): ', (projectVersion) => {
rl.question('Enter your name: ', (authorName) => {
// Set default version if not provided
if (!projectVersion.trim()) {
projectVersion = '1.0.0';
} }
// Otherwise, prompt the user for input
// Create readline interface only when needed
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
try {
const projectName = await promptQuestion(rl, 'Enter project name: ');
const projectDescription = await promptQuestion(rl, 'Enter project description: ');
const projectVersionInput = await promptQuestion(rl, 'Enter project version (default: 1.0.0): ');
const authorName = await promptQuestion(rl, 'Enter your name: ');
// Set default version if not provided
const projectVersion = projectVersionInput.trim() ? projectVersionInput : '1.0.0';
// Close the readline interface
rl.close();
// Create the project structure // Create the project structure
createProjectStructure(projectName, projectDescription, projectVersion, authorName); createProjectStructure(projectName, projectDescription, projectVersion, authorName);
rl.close(); return {
resolve({
projectName, projectName,
projectDescription, projectDescription,
projectVersion, projectVersion,
authorName authorName
}); };
}); } catch (error) {
}); // Make sure to close readline on error
}); rl.close();
}); throw error;
} }
}
// Helper function to promisify readline question
function promptQuestion(rl, question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
});
}); });
} }
@@ -199,22 +216,6 @@ function createProjectStructure(projectName, projectDescription, projectVersion,
// Create main README.md // Create main README.md
copyTemplateFile('README.md', path.join(targetDir, 'README.md'), replacements); copyTemplateFile('README.md', path.join(targetDir, 'README.md'), replacements);
// Create empty tasks.json
const tasksJson = {
meta: {
name: projectName,
version: projectVersion,
description: projectDescription
},
tasks: []
};
fs.writeFileSync(
path.join(targetDir, 'tasks.json'),
JSON.stringify(tasksJson, null, 2)
);
log('info', 'Created tasks.json');
// Initialize git repository if git is available // Initialize git repository if git is available
try { try {
if (!fs.existsSync(path.join(targetDir, '.git'))) { if (!fs.existsSync(path.join(targetDir, '.git'))) {
@@ -236,16 +237,28 @@ function createProjectStructure(projectName, projectDescription, projectVersion,
} }
// Run the initialization if this script is executed directly // Run the initialization if this script is executed directly
if (process.argv[1] === fileURLToPath(import.meta.url)) { // The original check doesn't work with npx and global commands
(async function main() { // if (process.argv[1] === fileURLToPath(import.meta.url)) {
// Instead, we'll always run the initialization if this file is the main module
console.log('Checking if script should run initialization...');
console.log('import.meta.url:', import.meta.url);
console.log('process.argv:', process.argv);
// Always run initialization when this file is loaded directly
// This works with both direct node execution and npx/global commands
(async function main() {
try { try {
console.log('Starting initialization...');
await initializeProject(); await initializeProject();
// Process should exit naturally after completion
console.log('Initialization completed, exiting...');
process.exit(0);
} catch (error) { } catch (error) {
console.error('Failed to initialize project:', error);
log('error', 'Failed to initialize project:', error); log('error', 'Failed to initialize project:', error);
process.exit(1); process.exit(1);
} }
})(); })();
}
// Export functions for programmatic use // Export functions for programmatic use
export { export {

View File

@@ -144,6 +144,17 @@ function preparePackage() {
process.exit(1); process.exit(1);
} }
// Make scripts executable
log('info', 'Making scripts executable...');
try {
execSync('chmod +x scripts/init.js', { stdio: 'ignore' });
log('info', 'Made scripts/init.js executable');
execSync('chmod +x scripts/dev.js', { stdio: 'ignore' });
log('info', 'Made scripts/dev.js executable');
} catch (error) {
log('error', 'Failed to make scripts executable:', error.message);
}
log('success', 'Package preparation completed successfully!'); log('success', 'Package preparation completed successfully!');
log('info', 'You can now publish the package with:'); log('info', 'You can now publish the package with:');
log('info', ' npm publish'); log('info', ' npm publish');