Compare commits
4 Commits
chore/pimp
...
fix/claude
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f5e0c73ec | ||
|
|
782728ff95 | ||
|
|
30ca144231 | ||
|
|
0220d0e994 |
12
.changeset/cute-files-pay.md
Normal file
12
.changeset/cute-files-pay.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add compact mode --compact / -c flag to the `tm list` CLI command
|
||||||
|
|
||||||
|
- outputs tasks in a minimal, git-style one-line format. This reduces verbose output from ~30+ lines of dashboards and tables to just 1 line per task, making it much easier to quickly scan available tasks.
|
||||||
|
- Git-style format: ID STATUS TITLE (PRIORITY) → DEPS
|
||||||
|
- Color-coded status, priority, and dependencies
|
||||||
|
- Smart title truncation and dependency abbreviation
|
||||||
|
- Subtask support with indentation
|
||||||
|
- Full backward compatibility with existing list options
|
||||||
5
.changeset/light-crabs-warn.md
Normal file
5
.changeset/light-crabs-warn.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"extension": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Display current task ID on task details page
|
||||||
5
.changeset/wet-seas-float.md
Normal file
5
.changeset/wet-seas-float.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove `clear` Taskmaster claude code commands since they were too close to the claude-code clear command
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
Clear all subtasks from all tasks globally.
|
|
||||||
|
|
||||||
## Global Subtask Clearing
|
|
||||||
|
|
||||||
Remove all subtasks across the entire project. Use with extreme caution.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Project-Wide Summary**
|
|
||||||
```
|
|
||||||
Global Subtask Summary
|
|
||||||
━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total parent tasks: 12
|
|
||||||
Total subtasks: 47
|
|
||||||
- Completed: 15
|
|
||||||
- In-progress: 8
|
|
||||||
- Pending: 24
|
|
||||||
|
|
||||||
Work at risk: ~120 hours
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Critical Warnings**
|
|
||||||
- In-progress subtasks that will lose work
|
|
||||||
- Completed subtasks with valuable history
|
|
||||||
- Complex dependency chains
|
|
||||||
- Integration test results
|
|
||||||
|
|
||||||
## Double Confirmation
|
|
||||||
|
|
||||||
```
|
|
||||||
⚠️ DESTRUCTIVE OPERATION WARNING ⚠️
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
This will remove ALL 47 subtasks from your project
|
|
||||||
Including 8 in-progress and 15 completed subtasks
|
|
||||||
|
|
||||||
This action CANNOT be undone
|
|
||||||
|
|
||||||
Type 'CLEAR ALL SUBTASKS' to confirm:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Safeguards
|
|
||||||
|
|
||||||
- Require explicit confirmation phrase
|
|
||||||
- Create automatic backup
|
|
||||||
- Log all removed data
|
|
||||||
- Option to export first
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
Valid reasons for global clear:
|
|
||||||
- Project restructuring
|
|
||||||
- Major pivot in approach
|
|
||||||
- Starting fresh breakdown
|
|
||||||
- Switching to different task organization
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Full project analysis
|
|
||||||
2. Create backup file
|
|
||||||
3. Show detailed impact
|
|
||||||
4. Require confirmation
|
|
||||||
5. Execute removal
|
|
||||||
6. Generate summary report
|
|
||||||
|
|
||||||
## Alternative Suggestions
|
|
||||||
|
|
||||||
Before clearing all:
|
|
||||||
- Export subtasks to file
|
|
||||||
- Clear only pending subtasks
|
|
||||||
- Clear by task category
|
|
||||||
- Archive instead of delete
|
|
||||||
|
|
||||||
## Post-Clear Report
|
|
||||||
|
|
||||||
```
|
|
||||||
Global Subtask Clear Complete
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Removed: 47 subtasks from 12 tasks
|
|
||||||
Backup saved: .taskmaster/backup/subtasks-20240115.json
|
|
||||||
Parent tasks updated: 12
|
|
||||||
Time estimates adjusted: Yes
|
|
||||||
|
|
||||||
Next steps:
|
|
||||||
- Review updated task list
|
|
||||||
- Re-expand complex tasks as needed
|
|
||||||
- Check project timeline
|
|
||||||
```
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
Clear all subtasks from a specific task.
|
|
||||||
|
|
||||||
Arguments: $ARGUMENTS (task ID)
|
|
||||||
|
|
||||||
Remove all subtasks from a parent task at once.
|
|
||||||
|
|
||||||
## Clearing Subtasks
|
|
||||||
|
|
||||||
Bulk removal of all subtasks from a parent task.
|
|
||||||
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
```bash
|
|
||||||
task-master clear-subtasks --id=<task-id>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-Clear Analysis
|
|
||||||
|
|
||||||
1. **Subtask Summary**
|
|
||||||
- Number of subtasks
|
|
||||||
- Completion status of each
|
|
||||||
- Work already done
|
|
||||||
- Dependencies affected
|
|
||||||
|
|
||||||
2. **Impact Assessment**
|
|
||||||
- Data that will be lost
|
|
||||||
- Dependencies to be removed
|
|
||||||
- Effect on project timeline
|
|
||||||
- Parent task implications
|
|
||||||
|
|
||||||
## Confirmation Required
|
|
||||||
|
|
||||||
```
|
|
||||||
Clear Subtasks Confirmation
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Parent Task: #5 "Implement user authentication"
|
|
||||||
Subtasks to remove: 4
|
|
||||||
- #5.1 "Setup auth framework" (done)
|
|
||||||
- #5.2 "Create login form" (in-progress)
|
|
||||||
- #5.3 "Add validation" (pending)
|
|
||||||
- #5.4 "Write tests" (pending)
|
|
||||||
|
|
||||||
⚠️ This will permanently delete all subtask data
|
|
||||||
Continue? (y/n)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smart Features
|
|
||||||
|
|
||||||
- Option to convert to standalone tasks
|
|
||||||
- Backup task data before clearing
|
|
||||||
- Preserve completed work history
|
|
||||||
- Update parent task appropriately
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. List all subtasks for confirmation
|
|
||||||
2. Check for in-progress work
|
|
||||||
3. Remove all subtasks
|
|
||||||
4. Update parent task
|
|
||||||
5. Clean up dependencies
|
|
||||||
|
|
||||||
## Alternative Options
|
|
||||||
|
|
||||||
Suggest alternatives:
|
|
||||||
- Convert important subtasks to tasks
|
|
||||||
- Keep completed subtasks
|
|
||||||
- Archive instead of delete
|
|
||||||
- Export subtask data first
|
|
||||||
|
|
||||||
## Post-Clear
|
|
||||||
|
|
||||||
- Show updated parent task
|
|
||||||
- Recalculate time estimates
|
|
||||||
- Update task complexity
|
|
||||||
- Suggest next steps
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```
|
|
||||||
/project:tm/clear-subtasks 5
|
|
||||||
→ Found 4 subtasks to remove
|
|
||||||
→ Warning: Subtask #5.2 is in-progress
|
|
||||||
→ Cleared all subtasks from task #5
|
|
||||||
→ Updated parent task estimates
|
|
||||||
→ Suggestion: Consider re-expanding with better breakdown
|
|
||||||
```
|
|
||||||
@@ -53,6 +53,11 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
|||||||
refreshComplexityAfterAI
|
refreshComplexityAfterAI
|
||||||
} = useTaskDetails({ taskId, sendMessage, tasks: allTasks });
|
} = useTaskDetails({ taskId, sendMessage, tasks: allTasks });
|
||||||
|
|
||||||
|
const displayId =
|
||||||
|
isSubtask && parentTask
|
||||||
|
? `${parentTask.id}.${currentTask?.id}`
|
||||||
|
: currentTask?.id;
|
||||||
|
|
||||||
const handleStatusChange = async (newStatus: TaskMasterTask['status']) => {
|
const handleStatusChange = async (newStatus: TaskMasterTask['status']) => {
|
||||||
if (!currentTask) return;
|
if (!currentTask) return;
|
||||||
|
|
||||||
@@ -60,10 +65,7 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
|||||||
await sendMessage({
|
await sendMessage({
|
||||||
type: 'updateTaskStatus',
|
type: 'updateTaskStatus',
|
||||||
data: {
|
data: {
|
||||||
taskId:
|
taskId: displayId,
|
||||||
isSubtask && parentTask
|
|
||||||
? `${parentTask.id}.${currentTask.id}`
|
|
||||||
: currentTask.id,
|
|
||||||
newStatus: newStatus
|
newStatus: newStatus
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -135,7 +137,7 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
|||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<span className="text-vscode-foreground">
|
<span className="text-vscode-foreground">
|
||||||
{currentTask.title}
|
#{displayId} {currentTask.title}
|
||||||
</span>
|
</span>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
@@ -152,9 +154,9 @@ export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task title */}
|
{/* Task ID and title */}
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-vscode-foreground">
|
<h1 className="text-2xl font-bold tracking-tight text-vscode-foreground">
|
||||||
{currentTask.title}
|
#{displayId} {currentTask.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Available Models as of August 8, 2025
|
# Available Models as of August 11, 2025
|
||||||
|
|
||||||
## Main Models
|
## Main Models
|
||||||
|
|
||||||
|
|||||||
@@ -1753,6 +1753,7 @@ function registerCommands(programInstance) {
|
|||||||
)
|
)
|
||||||
.option('-s, --status <status>', 'Filter by status')
|
.option('-s, --status <status>', 'Filter by status')
|
||||||
.option('--with-subtasks', 'Show subtasks for each task')
|
.option('--with-subtasks', 'Show subtasks for each task')
|
||||||
|
.option('-c, --compact', 'Display tasks in compact one-line format')
|
||||||
.option('--tag <tag>', 'Specify tag context for task operations')
|
.option('--tag <tag>', 'Specify tag context for task operations')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
// Initialize TaskMaster
|
// Initialize TaskMaster
|
||||||
@@ -1770,18 +1771,21 @@ function registerCommands(programInstance) {
|
|||||||
|
|
||||||
const statusFilter = options.status;
|
const statusFilter = options.status;
|
||||||
const withSubtasks = options.withSubtasks || false;
|
const withSubtasks = options.withSubtasks || false;
|
||||||
|
const compact = options.compact || false;
|
||||||
const tag = taskMaster.getCurrentTag();
|
const tag = taskMaster.getCurrentTag();
|
||||||
// Show current tag context
|
// Show current tag context
|
||||||
displayCurrentTagIndicator(tag);
|
displayCurrentTagIndicator(tag);
|
||||||
|
|
||||||
console.log(
|
if (!compact) {
|
||||||
chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`)
|
console.log(
|
||||||
);
|
chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`)
|
||||||
if (statusFilter) {
|
);
|
||||||
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
|
if (statusFilter) {
|
||||||
}
|
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
|
||||||
if (withSubtasks) {
|
}
|
||||||
console.log(chalk.blue('Including subtasks in listing'));
|
if (withSubtasks) {
|
||||||
|
console.log(chalk.blue('Including subtasks in listing'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await listTasks(
|
await listTasks(
|
||||||
@@ -1789,7 +1793,7 @@ function registerCommands(programInstance) {
|
|||||||
statusFilter,
|
statusFilter,
|
||||||
taskMaster.getComplexityReportPath(),
|
taskMaster.getComplexityReportPath(),
|
||||||
withSubtasks,
|
withSubtasks,
|
||||||
'text',
|
compact ? 'compact' : 'text',
|
||||||
{ projectRoot: taskMaster.getProjectRoot(), tag }
|
{ projectRoot: taskMaster.getProjectRoot(), tag }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -294,6 +294,11 @@ function listTasks(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For compact output, return minimal one-line format
|
||||||
|
if (outputFormat === 'compact') {
|
||||||
|
return renderCompactOutput(filteredTasks, withSubtasks);
|
||||||
|
}
|
||||||
|
|
||||||
// ... existing code for text output ...
|
// ... existing code for text output ...
|
||||||
|
|
||||||
// Calculate status breakdowns as percentages of total
|
// Calculate status breakdowns as percentages of total
|
||||||
@@ -962,4 +967,98 @@ function generateMarkdownOutput(data, filteredTasks, stats) {
|
|||||||
return markdown;
|
return markdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format dependencies for compact output with truncation and coloring
|
||||||
|
* @param {Array} dependencies - Array of dependency IDs
|
||||||
|
* @returns {string} - Formatted dependency string with arrow prefix
|
||||||
|
*/
|
||||||
|
function formatCompactDependencies(dependencies) {
|
||||||
|
if (!dependencies || dependencies.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dependencies.length > 5) {
|
||||||
|
const visible = dependencies.slice(0, 5).join(',');
|
||||||
|
const remaining = dependencies.length - 5;
|
||||||
|
return ` → ${chalk.cyan(visible)}${chalk.gray('... (+' + remaining + ' more)')}`;
|
||||||
|
} else {
|
||||||
|
return ` → ${chalk.cyan(dependencies.join(','))}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single task in compact one-line format
|
||||||
|
* @param {Object} task - Task object
|
||||||
|
* @param {number} maxTitleLength - Maximum title length before truncation
|
||||||
|
* @returns {string} - Formatted task line
|
||||||
|
*/
|
||||||
|
function formatCompactTask(task, maxTitleLength = 50) {
|
||||||
|
const status = task.status || 'pending';
|
||||||
|
const priority = task.priority || 'medium';
|
||||||
|
const title = truncate(task.title || 'Untitled', maxTitleLength);
|
||||||
|
|
||||||
|
// Use colored status from existing function
|
||||||
|
const coloredStatus = getStatusWithColor(status, true);
|
||||||
|
|
||||||
|
// Color priority based on level
|
||||||
|
const priorityColors = {
|
||||||
|
high: chalk.red,
|
||||||
|
medium: chalk.yellow,
|
||||||
|
low: chalk.gray
|
||||||
|
};
|
||||||
|
const priorityColor = priorityColors[priority] || chalk.white;
|
||||||
|
|
||||||
|
// Format dependencies using shared helper
|
||||||
|
const depsText = formatCompactDependencies(task.dependencies);
|
||||||
|
|
||||||
|
return `${chalk.cyan(task.id)} ${coloredStatus} ${chalk.white(title)} ${priorityColor('(' + priority + ')')}${depsText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a subtask in compact format with indentation
|
||||||
|
* @param {Object} subtask - Subtask object
|
||||||
|
* @param {string|number} parentId - Parent task ID
|
||||||
|
* @param {number} maxTitleLength - Maximum title length before truncation
|
||||||
|
* @returns {string} - Formatted subtask line
|
||||||
|
*/
|
||||||
|
function formatCompactSubtask(subtask, parentId, maxTitleLength = 47) {
|
||||||
|
const status = subtask.status || 'pending';
|
||||||
|
const title = truncate(subtask.title || 'Untitled', maxTitleLength);
|
||||||
|
|
||||||
|
// Use colored status from existing function
|
||||||
|
const coloredStatus = getStatusWithColor(status, true);
|
||||||
|
|
||||||
|
// Format dependencies using shared helper
|
||||||
|
const depsText = formatCompactDependencies(subtask.dependencies);
|
||||||
|
|
||||||
|
return ` ${chalk.cyan(parentId + '.' + subtask.id)} ${coloredStatus} ${chalk.dim(title)}${depsText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render complete compact output
|
||||||
|
* @param {Array} filteredTasks - Tasks to display
|
||||||
|
* @param {boolean} withSubtasks - Whether to include subtasks
|
||||||
|
* @returns {void} - Outputs directly to console
|
||||||
|
*/
|
||||||
|
function renderCompactOutput(filteredTasks, withSubtasks) {
|
||||||
|
if (filteredTasks.length === 0) {
|
||||||
|
console.log('No tasks found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
filteredTasks.forEach((task) => {
|
||||||
|
output.push(formatCompactTask(task));
|
||||||
|
|
||||||
|
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
|
||||||
|
task.subtasks.forEach((subtask) => {
|
||||||
|
output.push(formatCompactSubtask(subtask, task.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(output.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
export default listTasks;
|
export default listTasks;
|
||||||
|
|||||||
@@ -1430,6 +1430,20 @@ function ensureTagMetadata(tagObj, opts = {}) {
|
|||||||
return tagObj;
|
return tagObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI color codes from a string
|
||||||
|
* Useful for testing, logging to files, or when clean text output is needed
|
||||||
|
* @param {string} text - The text that may contain ANSI color codes
|
||||||
|
* @returns {string} - The text with ANSI color codes removed
|
||||||
|
*/
|
||||||
|
function stripAnsiCodes(text) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// Remove ANSI escape sequences (color codes, cursor movements, etc.)
|
||||||
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
// Export all utility functions and configuration
|
// Export all utility functions and configuration
|
||||||
export {
|
export {
|
||||||
LOG_LEVELS,
|
LOG_LEVELS,
|
||||||
@@ -1467,5 +1481,6 @@ export {
|
|||||||
markMigrationForNotice,
|
markMigrationForNotice,
|
||||||
flattenTasksWithSubtasks,
|
flattenTasksWithSubtasks,
|
||||||
ensureTagMetadata,
|
ensureTagMetadata,
|
||||||
|
stripAnsiCodes,
|
||||||
normalizeTaskIds
|
normalizeTaskIds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
|
|||||||
),
|
),
|
||||||
addComplexityToTask: jest.fn(),
|
addComplexityToTask: jest.fn(),
|
||||||
readComplexityReport: jest.fn(() => null),
|
readComplexityReport: jest.fn(() => null),
|
||||||
getTagAwareFilePath: jest.fn((tag, path) => '/mock/tagged/report.json')
|
getTagAwareFilePath: jest.fn((tag, path) => '/mock/tagged/report.json'),
|
||||||
|
stripAnsiCodes: jest.fn((text) =>
|
||||||
|
text ? text.replace(/\x1b\[[0-9;]*m/g, '') : text
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
||||||
@@ -45,8 +48,13 @@ jest.unstable_mockModule(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Import the mocked modules
|
// Import the mocked modules
|
||||||
const { readJSON, log, readComplexityReport, addComplexityToTask } =
|
const {
|
||||||
await import('../../../../../scripts/modules/utils.js');
|
readJSON,
|
||||||
|
log,
|
||||||
|
readComplexityReport,
|
||||||
|
addComplexityToTask,
|
||||||
|
stripAnsiCodes
|
||||||
|
} = await import('../../../../../scripts/modules/utils.js');
|
||||||
const { displayTaskList } = await import(
|
const { displayTaskList } = await import(
|
||||||
'../../../../../scripts/modules/ui.js'
|
'../../../../../scripts/modules/ui.js'
|
||||||
);
|
);
|
||||||
@@ -584,4 +592,140 @@ describe('listTasks', () => {
|
|||||||
expect(taskIds).toContain(5); // review task
|
expect(taskIds).toContain(5); // review task
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Compact output format', () => {
|
||||||
|
test('should output compact format when outputFormat is compact', async () => {
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const tasksPath = 'tasks/tasks.json';
|
||||||
|
|
||||||
|
await listTasks(tasksPath, null, null, false, 'compact', {
|
||||||
|
tag: 'master'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
|
||||||
|
// Strip ANSI color codes for testing
|
||||||
|
const cleanOutput = stripAnsiCodes(output);
|
||||||
|
|
||||||
|
// Should contain compact format elements: ID status title (priority) [→ dependencies]
|
||||||
|
expect(cleanOutput).toContain('1 done Setup Project (high)');
|
||||||
|
expect(cleanOutput).toContain(
|
||||||
|
'2 pending Implement Core Features (high) → 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format single task compactly', async () => {
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const tasksPath = 'tasks/tasks.json';
|
||||||
|
|
||||||
|
await listTasks(tasksPath, null, null, false, 'compact', {
|
||||||
|
tag: 'master'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
|
||||||
|
|
||||||
|
// Should be compact (no verbose headers)
|
||||||
|
expect(output).not.toContain('Project Dashboard');
|
||||||
|
expect(output).not.toContain('Progress:');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle compact format with subtasks', async () => {
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const tasksPath = 'tasks/tasks.json';
|
||||||
|
|
||||||
|
await listTasks(
|
||||||
|
tasksPath,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true, // withSubtasks = true
|
||||||
|
'compact',
|
||||||
|
{ tag: 'master' }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
|
||||||
|
// Strip ANSI color codes for testing
|
||||||
|
const cleanOutput = stripAnsiCodes(output);
|
||||||
|
|
||||||
|
// Should handle both tasks and subtasks
|
||||||
|
expect(cleanOutput).toContain('1 done Setup Project (high)');
|
||||||
|
expect(cleanOutput).toContain('3.1 done Create Header Component');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty task list in compact format', async () => {
|
||||||
|
readJSON.mockReturnValue({ tasks: [] });
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const tasksPath = 'tasks/tasks.json';
|
||||||
|
|
||||||
|
await listTasks(tasksPath, null, null, false, 'compact', {
|
||||||
|
tag: 'master'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('No tasks found');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should format dependencies correctly with shared helper', async () => {
|
||||||
|
// Create mock tasks with various dependency scenarios
|
||||||
|
const tasksWithDeps = {
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Task with no dependencies',
|
||||||
|
status: 'pending',
|
||||||
|
priority: 'medium',
|
||||||
|
dependencies: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Task with few dependencies',
|
||||||
|
status: 'pending',
|
||||||
|
priority: 'high',
|
||||||
|
dependencies: [1, 3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Task with many dependencies',
|
||||||
|
status: 'pending',
|
||||||
|
priority: 'low',
|
||||||
|
dependencies: [1, 2, 4, 5, 6, 7, 8, 9]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
readJSON.mockReturnValue(tasksWithDeps);
|
||||||
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const tasksPath = 'tasks/tasks.json';
|
||||||
|
|
||||||
|
await listTasks(tasksPath, null, null, false, 'compact', {
|
||||||
|
tag: 'master'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalled();
|
||||||
|
const output = consoleSpy.mock.calls.map((call) => call[0]).join('\n');
|
||||||
|
// Strip ANSI color codes for testing
|
||||||
|
const cleanOutput = stripAnsiCodes(output);
|
||||||
|
|
||||||
|
// Should format tasks correctly with compact output including priority
|
||||||
|
expect(cleanOutput).toContain(
|
||||||
|
'1 pending Task with no dependencies (medium)'
|
||||||
|
);
|
||||||
|
expect(cleanOutput).toContain('Task with few dependencies');
|
||||||
|
expect(cleanOutput).toContain('Task with many dependencies');
|
||||||
|
// Should show dependencies with arrow when they exist
|
||||||
|
expect(cleanOutput).toMatch(/2.*→.*1,3/);
|
||||||
|
// Should truncate many dependencies with "+X more" format
|
||||||
|
expect(cleanOutput).toMatch(/3.*→.*1,2,4,5,6.*\(\+\d+ more\)/);
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
56
tests/unit/utils-strip-ansi.test.js
Normal file
56
tests/unit/utils-strip-ansi.test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the stripAnsiCodes utility function
|
||||||
|
*/
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Import the module under test
|
||||||
|
const { stripAnsiCodes } = await import('../../scripts/modules/utils.js');
|
||||||
|
|
||||||
|
describe('stripAnsiCodes', () => {
|
||||||
|
test('should remove ANSI color codes from text', () => {
|
||||||
|
const textWithColors = '\x1b[31mRed text\x1b[0m \x1b[32mGreen text\x1b[0m';
|
||||||
|
const result = stripAnsiCodes(textWithColors);
|
||||||
|
expect(result).toBe('Red text Green text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle text without ANSI codes', () => {
|
||||||
|
const plainText = 'This is plain text';
|
||||||
|
const result = stripAnsiCodes(plainText);
|
||||||
|
expect(result).toBe('This is plain text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty string', () => {
|
||||||
|
const result = stripAnsiCodes('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle complex ANSI sequences', () => {
|
||||||
|
// Test with various ANSI escape sequences
|
||||||
|
const complexText =
|
||||||
|
'\x1b[1;31mBold red\x1b[0m \x1b[4;32mUnderlined green\x1b[0m \x1b[33;46mYellow on cyan\x1b[0m';
|
||||||
|
const result = stripAnsiCodes(complexText);
|
||||||
|
expect(result).toBe('Bold red Underlined green Yellow on cyan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle non-string input gracefully', () => {
|
||||||
|
expect(stripAnsiCodes(null)).toBe(null);
|
||||||
|
expect(stripAnsiCodes(undefined)).toBe(undefined);
|
||||||
|
expect(stripAnsiCodes(123)).toBe(123);
|
||||||
|
expect(stripAnsiCodes({})).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle real chalk output patterns', () => {
|
||||||
|
// Test patterns similar to what chalk produces
|
||||||
|
const chalkLikeText =
|
||||||
|
'1 \x1b[32m✓ done\x1b[39m Setup Project \x1b[31m(high)\x1b[39m';
|
||||||
|
const result = stripAnsiCodes(chalkLikeText);
|
||||||
|
expect(result).toBe('1 ✓ done Setup Project (high)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiline text with ANSI codes', () => {
|
||||||
|
const multilineText =
|
||||||
|
'\x1b[31mLine 1\x1b[0m\n\x1b[32mLine 2\x1b[0m\n\x1b[33mLine 3\x1b[0m';
|
||||||
|
const result = stripAnsiCodes(multilineText);
|
||||||
|
expect(result).toBe('Line 1\nLine 2\nLine 3');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user