feat(tagged-tasks): Complete core tag management system implementation
- Implements comprehensive tagged task lists system for multi-context task management including core tag management functions (Task 103.11), MCP integration updates, and foundational infrastructure for tagged task operations. Features tag CRUD operations, validation, metadata tracking, deep task copying, and full backward compatibility.
This commit is contained in:
@@ -2,29 +2,34 @@
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Introduces tagged task lists for multi-context task management
|
||||
Implements core tagged task lists system with full CLI support
|
||||
|
||||
This release adds support for organizing tasks into separate lists (tags) to enable working across multiple contexts such as different branches, environments, or project phases without conflicts.
|
||||
This release adds tagged task lists for multi-context task management, enabling users to organize tasks into separate contexts like "master", "feature-branch", teammate names or "v2" without conflicts.
|
||||
|
||||
**New Features:**
|
||||
|
||||
- **Tagged Task Lists**: Organize tasks into separate contexts with tags like "master", "feature-branch", or "v2.0"
|
||||
- **Complete Tag Management CLI**: Full suite of tag commands (`tags`, `add-tag`, `delete-tag`, `use-tag`, `rename-tag`, `copy-tag`)
|
||||
- **Tagged Task Lists**: Organize tasks into separate contexts with complete isolation between tags
|
||||
- **Seamless Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption
|
||||
- **Enhanced Delete Confirmation**: Double confirmation with inquirer prompts for destructive tag operations
|
||||
- **Master Tag Metadata**: Automatic metadata enhancement for existing tags with creation dates and descriptions
|
||||
- **Dynamic Task Counting**: Real-time task count calculation without stored counters
|
||||
- **Full Terminal Width Tables**: Responsive table layouts that adapt to terminal size
|
||||
- **Backward Compatibility**: All existing commands continue to work exactly as before
|
||||
- **Tag Management**: Commands to create, switch between, and manage different task contexts
|
||||
- **Git Integration**: Automatically create tags based on git branch names
|
||||
- **Context Isolation**: Tasks in different tags are completely separate and isolated
|
||||
|
||||
**Migration:**
|
||||
**Tag Management Commands:**
|
||||
|
||||
- Existing `tasks.json` files are automatically migrated to the new tagged format
|
||||
- Your tasks will appear under the "master" tag by default
|
||||
- No action required - migration happens transparently on first use
|
||||
- All existing workflows continue to work unchanged
|
||||
- `task-master tags [--show-metadata]` - List all available tags with task counts and metadata
|
||||
- `task-master add-tag <name> [--copy-from-current] [--copy-from=<tag>] [-d="<desc>"]` - Create new tag contexts
|
||||
- `task-master delete-tag <name> [--yes]` - Delete tags with double confirmation (changed from `--force` to `--yes`)
|
||||
- `task-master use-tag <name>` - Switch between tag contexts
|
||||
- `task-master rename-tag <old> <new>` - Rename existing tags
|
||||
- `task-master copy-tag <source> <target> [-d="<desc>"]` - Copy tags to create new contexts
|
||||
|
||||
**Coming in Part 2:**
|
||||
**Migration & Compatibility:**
|
||||
|
||||
- Existing `tasks.json` files are automatically migrated to tagged format
|
||||
- Master tag gets proper metadata (creation date, description) during migration
|
||||
- All existing workflows continue unchanged
|
||||
- Silent migration with user-friendly notifications
|
||||
|
||||
- CLI commands for tag management (`add-tag`, `use-tag`, `list-tags`)
|
||||
- MCP tool support for tag operations
|
||||
- Enhanced git branch integration
|
||||
- Tag switching and context management
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"currentTag": "master",
|
||||
"lastSwitched": "2025-06-11T20:26:12.598Z",
|
||||
"lastSwitched": "2025-06-12T05:28:34.147Z",
|
||||
"branchTagMapping": {},
|
||||
"migrationNoticeShown": true
|
||||
}
|
||||
@@ -49,11 +49,90 @@ Allow users to set and persist their preferred default tag in the global configu
|
||||
Added global.defaultTag configuration option to .taskmaster/config.json structure in assets/config.json. Implemented complete tags section including autoSwitchOnBranch and gitIntegration options. Created migrateConfigJson() function in utils.js to handle updating existing configuration files during the migration process. Configuration is automatically created and updated during the silent migration process to ensure seamless transition for existing users.
|
||||
</info added on 2025-06-11T20:46:57.669Z>
|
||||
|
||||
## 4. Develop Tag Management CLI Commands [pending]
|
||||
## 4. Develop Tag Management CLI Commands [done]
|
||||
### Dependencies: 103.1, 103.3
|
||||
### Description: Implement CLI commands for tag management: add-tag, delete, list, use (switch), rename, and copy, ensuring all changes are persisted.
|
||||
### Details:
|
||||
Each command should update the tasks.json and config files as needed. The primary command for creating tags should be 'add-tag' to maintain consistency with other task-master commands.
|
||||
<info added on 2025-06-12T07:14:51.761Z>
|
||||
✅ **COMPLETED: CLI Command Integration for Tag Management**
|
||||
|
||||
Successfully implemented complete CLI command integration for all tag management functions with enhanced UX features:
|
||||
|
||||
**Commands Implemented:**
|
||||
|
||||
1. **`task-master tags [--show-metadata]`** - List all available tags
|
||||
- Shows tag names, task counts, completion status
|
||||
- Optional metadata display (creation date, description)
|
||||
- Dynamic table width that adapts to terminal size
|
||||
- Current tag indicator with visual highlighting
|
||||
|
||||
2. **`task-master add-tag <name> [options]`** - Create new tags
|
||||
- `--copy-from-current` - Copy tasks from current tag
|
||||
- `--copy-from=<tag>` - Copy tasks from specified tag
|
||||
- `-d, --description <text>` - Set tag description
|
||||
- **Default behavior: Creates empty tags** (fixed from previous copying behavior)
|
||||
|
||||
3. **`task-master delete-tag <name> [--yes]`** - Delete tags with enhanced safety
|
||||
- **Changed from `--force` to `--yes`** for consistency
|
||||
- **Double confirmation system** using inquirer:
|
||||
- First: Yes/No confirmation prompt
|
||||
- Second: Type tag name to confirm deletion
|
||||
- Visual warning box showing impact
|
||||
- Automatic current tag switching if deleting active tag
|
||||
|
||||
4. **`task-master use-tag <name>`** - Switch tag contexts
|
||||
- Updates current tag in state.json
|
||||
- Validates tag existence before switching
|
||||
- Clear success messaging
|
||||
|
||||
5. **`task-master rename-tag <old> <new>`** - Rename existing tags
|
||||
- Validates both source and target names
|
||||
- Updates current tag reference if renaming active tag
|
||||
|
||||
6. **`task-master copy-tag <source> <target> [options]`** - Copy tags
|
||||
- `-d, --description <text>` - Set description for new tag
|
||||
- Deep copy of all tasks and metadata
|
||||
|
||||
**Key Improvements Made:**
|
||||
|
||||
Enhanced User Experience:
|
||||
- **Double confirmation for destructive operations** using inquirer prompts
|
||||
- **Consistent option naming** (`--yes` instead of `--force`)
|
||||
- **Dynamic table layouts** that use full terminal width
|
||||
- **Visual warning boxes** for dangerous operations
|
||||
- **Contextual help displays** on command errors
|
||||
|
||||
Technical Fixes:
|
||||
- **Fixed critical `_rawTaggedData` corruption bug** in readJSON/writeJSON cycle
|
||||
- **Dynamic task counting** instead of stored counters (eliminates sync issues)
|
||||
- **Master tag metadata enhancement** with creation dates and descriptions
|
||||
- **Proper error handling** with command-specific help displays
|
||||
|
||||
CLI Integration:
|
||||
- **Added all commands to help menu** in ui.js under "Tag Management" section
|
||||
- **Comprehensive help functions** for each command with examples
|
||||
- **Error handlers with contextual help** for better user guidance
|
||||
- **Consistent command patterns** following established CLI conventions
|
||||
|
||||
**Testing Completed:**
|
||||
- ✅ Created empty tags (default behavior)
|
||||
- ✅ Created tags with task copying (explicit flags)
|
||||
- ✅ Listed tags with and without metadata
|
||||
- ✅ Double confirmation for tag deletion
|
||||
- ✅ Tag switching and current tag persistence
|
||||
- ✅ Table width responsiveness
|
||||
- ✅ Master tag metadata enhancement
|
||||
- ✅ Error handling and help displays
|
||||
|
||||
**Files Modified:**
|
||||
- `scripts/modules/commands.js` - Added all tag management commands
|
||||
- `scripts/modules/task-manager/tag-management.js` - Enhanced functions with inquirer
|
||||
- `scripts/modules/ui.js` - Added tag commands to help menu
|
||||
- Fixed critical data corruption bug in utils.js
|
||||
|
||||
The CLI integration is now complete and production-ready with enhanced safety features and improved user experience!
|
||||
</info added on 2025-06-12T07:14:51.761Z>
|
||||
|
||||
## 5. Update Task Commands to Support --tag Flag [pending]
|
||||
### Dependencies: 103.4
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -75,7 +75,13 @@ export function registerAddDependencyTool(server) {
|
||||
}
|
||||
|
||||
// Use handleApiResult to format the response
|
||||
return handleApiResult(result, log, 'Error adding dependency');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error adding dependency',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in addDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -99,7 +99,13 @@ export function registerAddSubtaskTool(server) {
|
||||
log.error(`Failed to add subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error adding subtask');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error adding subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in addSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -99,7 +99,13 @@ export function registerAddTaskTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error adding task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in add-task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -135,7 +135,13 @@ export function registerAnalyzeProjectComplexityTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(result, log, 'Error analyzing task complexity');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error analyzing task complexity',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -74,7 +74,13 @@ export function registerClearSubtasksTool(server) {
|
||||
log.error(`Failed to clear subtasks: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error clearing subtasks');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error clearing subtasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in clearSubtasks tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -69,7 +69,9 @@ export function registerComplexityReportTool(server) {
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error retrieving complexity report'
|
||||
'Error retrieving complexity report',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in complexity-report tool: ${error.message}`);
|
||||
|
||||
@@ -92,7 +92,13 @@ export function registerExpandAllTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log, 'Error expanding all tasks');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error expanding all tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Unexpected error in expand_all tool execute: ${error.message}`
|
||||
|
||||
@@ -79,7 +79,13 @@ export function registerExpandTaskTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log, 'Error expanding task');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error expanding task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in expand-task tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -57,7 +57,13 @@ export function registerFixDependenciesTool(server) {
|
||||
log.error(`Failed to fix dependencies: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error fixing dependencies');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error fixing dependencies',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in fixDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -70,7 +70,13 @@ export function registerGenerateTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error generating task files');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error generating task files',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in generate tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -48,7 +48,13 @@ export function registerInitializeProjectTool(server) {
|
||||
|
||||
const result = await initializeProjectDirect(args, log, { session });
|
||||
|
||||
return handleApiResult(result, log, 'Initialization failed');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Initialization failed',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = `Project initialization tool failed: ${error.message || 'Unknown error'}`;
|
||||
log.error(errorMessage, error);
|
||||
|
||||
@@ -68,7 +68,13 @@ export function registerModelsTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error managing models',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in models tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -102,7 +102,10 @@ export function registerMoveTaskTool(server) {
|
||||
message: `Successfully moved ${results.length} tasks`
|
||||
}
|
||||
},
|
||||
log
|
||||
log,
|
||||
'Error moving multiple tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} else {
|
||||
// Moving a single task
|
||||
@@ -117,7 +120,10 @@ export function registerMoveTaskTool(server) {
|
||||
log,
|
||||
{ session }
|
||||
),
|
||||
log
|
||||
log,
|
||||
'Error moving task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -70,7 +70,13 @@ export function registerNextTaskTool(server) {
|
||||
);
|
||||
|
||||
log.info(`Next task result: ${result.success ? 'found' : 'none'}`);
|
||||
return handleApiResult(result, log, 'Error finding next task');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error finding next task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error finding next task: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -64,7 +64,13 @@ export function registerParsePRDTool(server) {
|
||||
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
|
||||
try {
|
||||
const result = await parsePRDDirect(args, log, { session });
|
||||
return handleApiResult(result, log);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error parsing PRD',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in parse_prd: ${error.message}`);
|
||||
return createErrorResponse(`Failed to parse PRD: ${error.message}`);
|
||||
|
||||
@@ -68,7 +68,13 @@ export function registerRemoveDependencyTool(server) {
|
||||
log.error(`Failed to remove dependency: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error removing dependency');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error removing dependency',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in removeDependency tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -80,7 +80,13 @@ export function registerRemoveSubtaskTool(server) {
|
||||
log.error(`Failed to remove subtask: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error removing subtask');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error removing subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in removeSubtask tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -69,7 +69,13 @@ export function registerRemoveTaskTool(server) {
|
||||
log.error(`Failed to remove task: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error removing task');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error removing task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in remove-task tool: ${error.message}`);
|
||||
return createErrorResponse(`Failed to remove task: ${error.message}`);
|
||||
|
||||
@@ -72,7 +72,13 @@ export function registerResearchTool(server) {
|
||||
{ session }
|
||||
);
|
||||
|
||||
return handleApiResult(result, log);
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error performing research',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in research tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -100,7 +100,13 @@ export function registerSetTaskStatusTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error setting task status');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error setting task status',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in setTaskStatus tool: ${error.message}`);
|
||||
return createErrorResponse(
|
||||
|
||||
@@ -75,7 +75,13 @@ export function registerUpdateSubtaskTool(server) {
|
||||
);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error updating subtask');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error updating subtask',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -77,7 +77,13 @@ export function registerUpdateTaskTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(result, log, 'Error updating task');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error updating task',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -80,7 +80,13 @@ export function registerUpdateTool(server) {
|
||||
log.info(
|
||||
`${toolName}: Direct function result: success=${result.success}`
|
||||
);
|
||||
return handleApiResult(result, log, 'Error updating tasks');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error updating tasks',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Critical error in ${toolName} tool execute: ${error.message}`
|
||||
|
||||
@@ -60,7 +60,13 @@ export function registerValidateDependenciesTool(server) {
|
||||
log.error(`Failed to validate dependencies: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return handleApiResult(result, log, 'Error validating dependencies');
|
||||
return handleApiResult(
|
||||
result,
|
||||
log,
|
||||
'Error validating dependencies',
|
||||
undefined,
|
||||
args.projectRoot
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(`Error in validateDependencies tool: ${error.message}`);
|
||||
return createErrorResponse(error.message);
|
||||
|
||||
@@ -36,6 +36,15 @@ import {
|
||||
migrateProject
|
||||
} from './task-manager.js';
|
||||
|
||||
import {
|
||||
createTag,
|
||||
deleteTag,
|
||||
tags,
|
||||
useTag,
|
||||
renameTag,
|
||||
copyTag
|
||||
} from './task-manager/tag-management.js';
|
||||
|
||||
import {
|
||||
addDependency,
|
||||
removeDependency,
|
||||
@@ -2348,6 +2357,117 @@ ${result.result}
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to show tags command help
|
||||
function showTagsHelp() {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Tags Command Help') +
|
||||
'\n\n' +
|
||||
chalk.cyan('Usage:') +
|
||||
'\n' +
|
||||
` task-master tags [options]\n\n` +
|
||||
chalk.cyan('Options:') +
|
||||
'\n' +
|
||||
' -f, --file <file> Path to the tasks file (default: "' +
|
||||
TASKMASTER_TASKS_FILE +
|
||||
'")\n' +
|
||||
' --show-metadata Show detailed metadata for each tag\n\n' +
|
||||
chalk.cyan('Examples:') +
|
||||
'\n' +
|
||||
' task-master tags\n' +
|
||||
' task-master tags --show-metadata\n\n' +
|
||||
chalk.cyan('Related Commands:') +
|
||||
'\n' +
|
||||
' task-master add-tag <name> Create a new tag\n' +
|
||||
' task-master use-tag <name> Switch to a tag\n' +
|
||||
' task-master delete-tag <name> Delete a tag',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to show add-tag command help
|
||||
function showAddTagHelp() {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Add Tag Command Help') +
|
||||
'\n\n' +
|
||||
chalk.cyan('Usage:') +
|
||||
'\n' +
|
||||
` task-master add-tag <tagName> [options]\n\n` +
|
||||
chalk.cyan('Options:') +
|
||||
'\n' +
|
||||
' -f, --file <file> Path to the tasks file (default: "' +
|
||||
TASKMASTER_TASKS_FILE +
|
||||
'")\n' +
|
||||
' --copy-from-current Copy tasks from the current tag to the new tag\n' +
|
||||
' --copy-from <tag> Copy tasks from the specified tag to the new tag\n' +
|
||||
' -d, --description <text> Optional description for the tag\n\n' +
|
||||
chalk.cyan('Examples:') +
|
||||
'\n' +
|
||||
' task-master add-tag feature-xyz\n' +
|
||||
' task-master add-tag feature-xyz --copy-from-current\n' +
|
||||
' task-master add-tag feature-xyz --copy-from master\n' +
|
||||
' task-master add-tag feature-xyz -d "Feature XYZ development"',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to show delete-tag command help
|
||||
function showDeleteTagHelp() {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Delete Tag Command Help') +
|
||||
'\n\n' +
|
||||
chalk.cyan('Usage:') +
|
||||
'\n' +
|
||||
` task-master delete-tag <tagName> [options]\n\n` +
|
||||
chalk.cyan('Options:') +
|
||||
'\n' +
|
||||
' -f, --file <file> Path to the tasks file (default: "' +
|
||||
TASKMASTER_TASKS_FILE +
|
||||
'")\n' +
|
||||
' -y, --yes Skip confirmation prompts\n\n' +
|
||||
chalk.cyan('Examples:') +
|
||||
'\n' +
|
||||
' task-master delete-tag feature-xyz\n' +
|
||||
' task-master delete-tag feature-xyz --yes\n\n' +
|
||||
chalk.yellow('Warning:') +
|
||||
'\n' +
|
||||
' This will permanently delete the tag and all its tasks!',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to show use-tag command help
|
||||
function showUseTagHelp() {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Use Tag Command Help') +
|
||||
'\n\n' +
|
||||
chalk.cyan('Usage:') +
|
||||
'\n' +
|
||||
` task-master use-tag <tagName> [options]\n\n` +
|
||||
chalk.cyan('Options:') +
|
||||
'\n' +
|
||||
' -f, --file <file> Path to the tasks file (default: "' +
|
||||
TASKMASTER_TASKS_FILE +
|
||||
'")\n\n' +
|
||||
chalk.cyan('Examples:') +
|
||||
'\n' +
|
||||
' task-master use-tag feature-xyz\n' +
|
||||
' task-master use-tag master\n\n' +
|
||||
chalk.cyan('Related Commands:') +
|
||||
'\n' +
|
||||
' task-master tags List all available tags\n' +
|
||||
' task-master add-tag <name> Create a new tag',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// remove-task command
|
||||
programInstance
|
||||
.command('remove-task')
|
||||
@@ -3071,6 +3191,330 @@ Examples:
|
||||
}
|
||||
});
|
||||
|
||||
// ===== TAG MANAGEMENT COMMANDS =====
|
||||
|
||||
// add-tag command
|
||||
programInstance
|
||||
.command('add-tag')
|
||||
.description('Create a new tag context for organizing tasks')
|
||||
.argument('<tagName>', 'Name of the new tag to create')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option(
|
||||
'--copy-from-current',
|
||||
'Copy tasks from the current tag to the new tag'
|
||||
)
|
||||
.option(
|
||||
'--copy-from <tag>',
|
||||
'Copy tasks from the specified tag to the new tag'
|
||||
)
|
||||
.option('-d, --description <text>', 'Optional description for the tag')
|
||||
.action(async (tagName, options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const createOptions = {
|
||||
copyFromCurrent: options.copyFromCurrent || false,
|
||||
copyFromTag: options.copyFrom,
|
||||
description: options.description
|
||||
};
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'add-tag',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await createTag(tasksPath, tagName, createOptions, context, 'text');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error creating tag: ${error.message}`));
|
||||
showAddTagHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
showAddTagHelp();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// delete-tag command
|
||||
programInstance
|
||||
.command('delete-tag')
|
||||
.description('Delete an existing tag and all its tasks')
|
||||
.argument('<tagName>', 'Name of the tag to delete')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option('-y, --yes', 'Skip confirmation prompts')
|
||||
.action(async (tagName, options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const deleteOptions = {
|
||||
yes: options.yes || false
|
||||
};
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'delete-tag',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await deleteTag(tasksPath, tagName, deleteOptions, context, 'text');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error deleting tag: ${error.message}`));
|
||||
showDeleteTagHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
showDeleteTagHelp();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// tags command
|
||||
programInstance
|
||||
.command('tags')
|
||||
.description('List all available tags with metadata')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option('--show-metadata', 'Show detailed metadata for each tag')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const listOptions = {
|
||||
showTaskCounts: true,
|
||||
showMetadata: options.showMetadata || false
|
||||
};
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'tags',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await tags(tasksPath, listOptions, context, 'text');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error listing tags: ${error.message}`));
|
||||
showTagsHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
showTagsHelp();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// use-tag command
|
||||
programInstance
|
||||
.command('use-tag')
|
||||
.description('Switch to a different tag context')
|
||||
.argument('<tagName>', 'Name of the tag to switch to')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.action(async (tagName, options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'use-tag',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await useTag(tasksPath, tagName, {}, context, 'text');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error switching tag: ${error.message}`));
|
||||
showUseTagHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
showUseTagHelp();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// rename-tag command
|
||||
programInstance
|
||||
.command('rename-tag')
|
||||
.description('Rename an existing tag')
|
||||
.argument('<oldName>', 'Current name of the tag')
|
||||
.argument('<newName>', 'New name for the tag')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.action(async (oldName, newName, options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'rename-tag',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await renameTag(tasksPath, oldName, newName, {}, context, 'text');
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error renaming tag: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// copy-tag command
|
||||
programInstance
|
||||
.command('copy-tag')
|
||||
.description('Copy an existing tag to create a new tag with the same tasks')
|
||||
.argument('<sourceName>', 'Name of the source tag to copy from')
|
||||
.argument('<targetName>', 'Name of the new tag to create')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option('-d, --description <text>', 'Optional description for the new tag')
|
||||
.action(async (sourceName, targetName, options) => {
|
||||
try {
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(chalk.red('Error: Could not find project root.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = path.resolve(projectRoot, options.file);
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
console.error(
|
||||
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const copyOptions = {
|
||||
description: options.description
|
||||
};
|
||||
|
||||
const context = {
|
||||
projectRoot,
|
||||
commandName: 'copy-tag',
|
||||
outputType: 'cli'
|
||||
};
|
||||
|
||||
await copyTag(
|
||||
tasksPath,
|
||||
sourceName,
|
||||
targetName,
|
||||
copyOptions,
|
||||
context,
|
||||
'text'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error copying tag: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.on('error', function (err) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
@@ -3105,8 +3549,15 @@ function setupCLI() {
|
||||
.helpOption('-h, --help', 'Display help')
|
||||
.addHelpCommand(false); // Disable default help command
|
||||
|
||||
// Modify the help option to use your custom display
|
||||
programInstance.helpInformation = () => {
|
||||
// Only override help for the main program, not for individual commands
|
||||
const originalHelpInformation =
|
||||
programInstance.helpInformation.bind(programInstance);
|
||||
programInstance.helpInformation = function () {
|
||||
// If this is being called for a subcommand, use the default Commander.js help
|
||||
if (this.parent && this.parent !== programInstance) {
|
||||
return originalHelpInformation();
|
||||
}
|
||||
// If this is the main program help, use our custom display
|
||||
displayHelp();
|
||||
return '';
|
||||
};
|
||||
|
||||
1101
scripts/modules/task-manager/tag-management.js
Normal file
1101
scripts/modules/task-manager/tag-management.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -659,6 +659,42 @@ function displayHelp() {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Tag Management',
|
||||
color: 'magenta',
|
||||
commands: [
|
||||
{
|
||||
name: 'tags',
|
||||
args: '[--show-metadata]',
|
||||
desc: 'List all available tags with task counts'
|
||||
},
|
||||
{
|
||||
name: 'add-tag',
|
||||
args: '<tagName> [--copy-from-current] [--copy-from=<tag>] [-d="<desc>"]',
|
||||
desc: 'Create a new tag context for organizing tasks'
|
||||
},
|
||||
{
|
||||
name: 'use-tag',
|
||||
args: '<tagName>',
|
||||
desc: 'Switch to a different tag context'
|
||||
},
|
||||
{
|
||||
name: 'delete-tag',
|
||||
args: '<tagName> [--yes]',
|
||||
desc: 'Delete an existing tag and all its tasks'
|
||||
},
|
||||
{
|
||||
name: 'rename-tag',
|
||||
args: '<oldName> <newName>',
|
||||
desc: 'Rename an existing tag'
|
||||
},
|
||||
{
|
||||
name: 'copy-tag',
|
||||
args: '<sourceName> <targetName> [-d="<desc>"]',
|
||||
desc: 'Copy an existing tag to create a new tag with the same tasks'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Dependency Management',
|
||||
color: 'blue',
|
||||
|
||||
@@ -231,9 +231,24 @@ function readJSON(filepath, projectRoot = null) {
|
||||
filepath.includes('tasks.json')
|
||||
) {
|
||||
// This is legacy format - migrate to tagged format
|
||||
// Get file creation/modification date for the master tag
|
||||
let createdDate;
|
||||
try {
|
||||
const stats = fs.statSync(filepath);
|
||||
// Use the earlier of creation time or modification time
|
||||
createdDate =
|
||||
stats.birthtime < stats.mtime ? stats.birthtime : stats.mtime;
|
||||
} catch (error) {
|
||||
// Fallback to current date if we can't get file stats
|
||||
createdDate = new Date();
|
||||
}
|
||||
|
||||
const migratedData = {
|
||||
master: {
|
||||
tasks: data.tasks
|
||||
tasks: data.tasks,
|
||||
description: 'Tasks live here by default',
|
||||
created: createdDate.toISOString(),
|
||||
taskCount: data.tasks ? data.tasks.length : 0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -312,22 +327,25 @@ function readJSON(filepath, projectRoot = null) {
|
||||
resolvedTag = 'master';
|
||||
}
|
||||
|
||||
// Store the raw tagged data BEFORE modifying data
|
||||
const rawTaggedData = { ...data };
|
||||
|
||||
// Return the tasks for the resolved tag, or master as fallback, or empty array
|
||||
if (data[resolvedTag] && data[resolvedTag].tasks) {
|
||||
data = {
|
||||
tag: resolvedTag,
|
||||
tasks: data[resolvedTag].tasks,
|
||||
_rawTaggedData: data
|
||||
_rawTaggedData: rawTaggedData
|
||||
};
|
||||
} else if (data.master && data.master.tasks) {
|
||||
data = {
|
||||
tag: 'master',
|
||||
tasks: data.master.tasks,
|
||||
_rawTaggedData: data
|
||||
_rawTaggedData: rawTaggedData
|
||||
};
|
||||
} else {
|
||||
// No valid tags found, return empty
|
||||
data = { tasks: [], tag: 'master', _rawTaggedData: data };
|
||||
data = { tasks: [], tag: 'master', _rawTaggedData: rawTaggedData };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -512,7 +530,16 @@ function writeJSON(filepath, data) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8');
|
||||
|
||||
// Clean the data before writing - remove internal properties that should not be persisted
|
||||
let cleanData = data;
|
||||
if (data && typeof data === 'object' && data._rawTaggedData !== undefined) {
|
||||
// Create a clean copy without internal properties using destructuring
|
||||
const { _rawTaggedData, ...cleanedData } = data;
|
||||
cleanData = cleanedData;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), 'utf8');
|
||||
} catch (error) {
|
||||
log('error', `Error writing JSON file ${filepath}:`, error.message);
|
||||
if (isDebug) {
|
||||
|
||||
Reference in New Issue
Block a user