feat: improve table UI

This commit is contained in:
Ralph Khreish
2025-09-13 10:14:13 -07:00
parent ff9b7a46d6
commit cd2b4c9d56
8 changed files with 448 additions and 512 deletions

View File

@@ -6,10 +6,7 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./src/index.ts", "types": "./src/index.ts",
"exports": { "exports": {
".": { ".": "./src/index.ts"
"types": "./src/index.ts",
"import": "./dist/index.js"
}
}, },
"files": ["dist", "README.md"], "files": ["dist", "README.md"],
"scripts": { "scripts": {
@@ -46,5 +43,10 @@
}, },
"keywords": ["task-master", "cli", "task-management", "productivity"], "keywords": ["task-master", "cli", "task-management", "productivity"],
"author": "", "author": "",
"license": "MIT" "license": "MIT",
"typesVersions": {
"*": {
"*": ["src/*"]
}
}
} }

View File

@@ -280,16 +280,8 @@ export class ListTasksCommand extends Command {
const depStats = calculateDependencyStatistics(tasks); const depStats = calculateDependencyStatistics(tasks);
const priorityBreakdown = getPriorityBreakdown(tasks); const priorityBreakdown = getPriorityBreakdown(tasks);
// Find next task (simplified for now) // Find next task following the same logic as findNextTask
const nextTask: NextTaskInfo | undefined = tasks const nextTask = this.findNextTask(tasks);
.filter(t => t.status === 'pending' && (!t.dependencies || t.dependencies.length === 0))
.map(t => ({
id: t.id,
title: t.title,
priority: t.priority,
dependencies: t.dependencies,
complexity: undefined // Add if available
}))[0];
// Display dashboard boxes // Display dashboard boxes
displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask); displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask);
@@ -311,6 +303,123 @@ export class ListTasksCommand extends Command {
this.lastResult = result; this.lastResult = result;
} }
/**
* Find the next task to work on
* Implements the same logic as scripts/modules/task-manager/find-next-task.js
*/
private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
const priorityValues: Record<string, number> = {
critical: 4,
high: 3,
medium: 2,
low: 1
};
// Build set of completed task IDs (including subtasks)
const completedIds = new Set<string>();
tasks.forEach(t => {
if (t.status === 'done' || t.status === 'completed') {
completedIds.add(String(t.id));
}
if (t.subtasks) {
t.subtasks.forEach(st => {
if (st.status === 'done' || st.status === 'completed') {
completedIds.add(`${t.id}.${st.id}`);
}
});
}
});
// First, look for eligible subtasks in in-progress parent tasks
const candidateSubtasks: NextTaskInfo[] = [];
tasks
.filter(t => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0)
.forEach(parent => {
parent.subtasks!.forEach(st => {
const stStatus = (st.status || 'pending').toLowerCase();
if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
// Check if dependencies are satisfied
const fullDeps = st.dependencies?.map(d => {
// Handle both numeric and string IDs
if (typeof d === 'string' && d.includes('.')) {
return d;
}
return `${parent.id}.${d}`;
}) ?? [];
const depsSatisfied = fullDeps.length === 0 ||
fullDeps.every(depId => completedIds.has(String(depId)));
if (depsSatisfied) {
candidateSubtasks.push({
id: `${parent.id}.${st.id}`,
title: st.title || `Subtask ${st.id}`,
priority: st.priority || parent.priority || 'medium',
dependencies: fullDeps.map(d => String(d))
});
}
});
});
if (candidateSubtasks.length > 0) {
// Sort by priority, then by dependencies count, then by ID
candidateSubtasks.sort((a, b) => {
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
return String(a.id).localeCompare(String(b.id));
});
return candidateSubtasks[0];
}
// Fall back to finding eligible top-level tasks
const eligibleTasks = tasks.filter(task => {
// Skip non-eligible statuses
const status = (task.status || 'pending').toLowerCase();
if (status !== 'pending' && status !== 'in-progress') return false;
// Check dependencies
const deps = task.dependencies || [];
const depsSatisfied = deps.length === 0 ||
deps.every(depId => completedIds.has(String(depId)));
return depsSatisfied;
});
if (eligibleTasks.length === 0) return undefined;
// Sort eligible tasks
eligibleTasks.sort((a, b) => {
// Priority (higher first)
const pa = priorityValues[a.priority || 'medium'] ?? 2;
const pb = priorityValues[b.priority || 'medium'] ?? 2;
if (pb !== pa) return pb - pa;
// Dependencies count (fewer first)
const depCountA = a.dependencies?.length || 0;
const depCountB = b.dependencies?.length || 0;
if (depCountA !== depCountB) return depCountA - depCountB;
// ID (lower first)
return Number(a.id) - Number(b.id);
});
const nextTask = eligibleTasks[0];
return {
id: nextTask.id,
title: nextTask.title,
priority: nextTask.priority,
dependencies: nextTask.dependencies?.map(d => String(d))
};
}
/** /**
* Get the last result (for programmatic usage) * Get the last result (for programmatic usage)
*/ */

View File

@@ -46,13 +46,113 @@ export interface NextTaskInfo {
} }
/** /**
* Create a progress bar with percentage * Status breakdown for progress bars
*/ */
function createProgressBar(percentage: number, width: number = 30): string { export interface StatusBreakdown {
const filled = Math.round((percentage / 100) * width); 'in-progress'?: number;
const empty = width - filled; pending?: number;
blocked?: number;
const bar = chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty); deferred?: number;
cancelled?: number;
review?: number;
}
/**
* Create a progress bar with color-coded status segments
*/
function createProgressBar(
completionPercentage: number,
width: number = 30,
statusBreakdown?: StatusBreakdown
): string {
// If no breakdown provided, use simple green bar
if (!statusBreakdown) {
const filled = Math.round((completionPercentage / 100) * width);
const empty = width - filled;
return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
}
// Build the bar with different colored sections
// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
let bar = '';
let charsUsed = 0;
// 1. Green filled blocks for completed tasks (done)
const completedChars = Math.round((completionPercentage / 100) * width);
if (completedChars > 0) {
bar += chalk.green('█').repeat(completedChars);
charsUsed += completedChars;
}
// 2. Gray filled blocks for cancelled (won't be done)
if (statusBreakdown.cancelled && charsUsed < width) {
const cancelledChars = Math.round(
(statusBreakdown.cancelled / 100) * width
);
const actualChars = Math.min(cancelledChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 3. Gray filled blocks for deferred (won't be done now)
if (statusBreakdown.deferred && charsUsed < width) {
const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
const actualChars = Math.min(deferredChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.gray('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 4. Blue filled blocks for in-progress (actively working)
if (statusBreakdown['in-progress'] && charsUsed < width) {
const inProgressChars = Math.round(
(statusBreakdown['in-progress'] / 100) * width
);
const actualChars = Math.min(inProgressChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.blue('█').repeat(actualChars);
charsUsed += actualChars;
}
}
// 5. Magenta empty blocks for review (almost done)
if (statusBreakdown.review && charsUsed < width) {
const reviewChars = Math.round((statusBreakdown.review / 100) * width);
const actualChars = Math.min(reviewChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.magenta('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 6. Yellow empty blocks for pending (ready to start)
if (statusBreakdown.pending && charsUsed < width) {
const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
const actualChars = Math.min(pendingChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.yellow('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// 7. Red empty blocks for blocked (can't start yet)
if (statusBreakdown.blocked && charsUsed < width) {
const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
const actualChars = Math.min(blockedChars, width - charsUsed);
if (actualChars > 0) {
bar += chalk.red('░').repeat(actualChars);
charsUsed += actualChars;
}
}
// Fill any remaining space with gray empty yellow blocks
if (charsUsed < width) {
bar += chalk.yellow('░').repeat(width - charsUsed);
}
return bar; return bar;
} }
@@ -72,7 +172,7 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
completionPercentage: 0 completionPercentage: 0
}; };
tasks.forEach(task => { tasks.forEach((task) => {
switch (task.status) { switch (task.status) {
case 'done': case 'done':
stats.done++; stats.done++;
@@ -98,9 +198,8 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics {
} }
}); });
stats.completionPercentage = stats.total > 0 stats.completionPercentage =
? Math.round((stats.done / stats.total) * 100) stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
: 0;
return stats; return stats;
} }
@@ -121,9 +220,9 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
completionPercentage: 0 completionPercentage: 0
}; };
tasks.forEach(task => { tasks.forEach((task) => {
if (task.subtasks && task.subtasks.length > 0) { if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach(subtask => { task.subtasks.forEach((subtask) => {
stats.total++; stats.total++;
switch (subtask.status) { switch (subtask.status) {
case 'done': case 'done':
@@ -152,9 +251,8 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
} }
}); });
stats.completionPercentage = stats.total > 0 stats.completionPercentage =
? Math.round((stats.done / stats.total) * 100) stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0;
: 0;
return stats; return stats;
} }
@@ -162,34 +260,39 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics {
/** /**
* Calculate dependency statistics * Calculate dependency statistics
*/ */
export function calculateDependencyStatistics(tasks: Task[]): DependencyStatistics { export function calculateDependencyStatistics(
tasks: Task[]
): DependencyStatistics {
const completedTaskIds = new Set( const completedTaskIds = new Set(
tasks.filter(t => t.status === 'done').map(t => t.id) tasks.filter((t) => t.status === 'done').map((t) => t.id)
); );
const tasksWithNoDeps = tasks.filter( const tasksWithNoDeps = tasks.filter(
t => t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) (t) =>
t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0)
).length; ).length;
const tasksWithAllDepsSatisfied = tasks.filter( const tasksWithAllDepsSatisfied = tasks.filter(
t => t.status !== 'done' && (t) =>
t.status !== 'done' &&
t.dependencies && t.dependencies &&
t.dependencies.length > 0 && t.dependencies.length > 0 &&
t.dependencies.every(depId => completedTaskIds.has(depId)) t.dependencies.every((depId) => completedTaskIds.has(depId))
).length; ).length;
const tasksBlockedByDeps = tasks.filter( const tasksBlockedByDeps = tasks.filter(
t => t.status !== 'done' && (t) =>
t.status !== 'done' &&
t.dependencies && t.dependencies &&
t.dependencies.length > 0 && t.dependencies.length > 0 &&
!t.dependencies.every(depId => completedTaskIds.has(depId)) !t.dependencies.every((depId) => completedTaskIds.has(depId))
).length; ).length;
// Calculate most depended-on task // Calculate most depended-on task
const dependencyCount: Record<string, number> = {}; const dependencyCount: Record<string, number> = {};
tasks.forEach(task => { tasks.forEach((task) => {
if (task.dependencies && task.dependencies.length > 0) { if (task.dependencies && task.dependencies.length > 0) {
task.dependencies.forEach(depId => { task.dependencies.forEach((depId) => {
const key = String(depId); const key = String(depId);
dependencyCount[key] = (dependencyCount[key] || 0) + 1; dependencyCount[key] = (dependencyCount[key] || 0) + 1;
}); });
@@ -198,7 +301,7 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti
let mostDependedOnTaskId: number | undefined; let mostDependedOnTaskId: number | undefined;
let mostDependedOnCount = 0; let mostDependedOnCount = 0;
for (const [taskId, count] of Object.entries(dependencyCount)) { for (const [taskId, count] of Object.entries(dependencyCount)) {
if (count > mostDependedOnCount) { if (count > mostDependedOnCount) {
mostDependedOnCount = count; mostDependedOnCount = count;
@@ -211,9 +314,8 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti
(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
0 0
); );
const avgDependenciesPerTask = tasks.length > 0 const avgDependenciesPerTask =
? totalDependencies / tasks.length tasks.length > 0 ? totalDependencies / tasks.length : 0;
: 0;
return { return {
tasksWithNoDeps, tasksWithNoDeps,
@@ -228,7 +330,9 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti
/** /**
* Get priority counts * Get priority counts
*/ */
export function getPriorityBreakdown(tasks: Task[]): Record<TaskPriority, number> { export function getPriorityBreakdown(
tasks: Task[]
): Record<TaskPriority, number> {
const breakdown: Record<TaskPriority, number> = { const breakdown: Record<TaskPriority, number> = {
critical: 0, critical: 0,
high: 0, high: 0,
@@ -236,7 +340,7 @@ export function getPriorityBreakdown(tasks: Task[]): Record<TaskPriority, number
low: 0 low: 0
}; };
tasks.forEach(task => { tasks.forEach((task) => {
const priority = task.priority || 'medium'; const priority = task.priority || 'medium';
breakdown[priority]++; breakdown[priority]++;
}); });
@@ -244,6 +348,57 @@ export function getPriorityBreakdown(tasks: Task[]): Record<TaskPriority, number
return breakdown; return breakdown;
} }
/**
* Calculate status breakdown as percentages
*/
function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
if (stats.total === 0) return {};
return {
'in-progress': (stats.inProgress / stats.total) * 100,
pending: (stats.pending / stats.total) * 100,
blocked: (stats.blocked / stats.total) * 100,
deferred: (stats.deferred / stats.total) * 100,
cancelled: (stats.cancelled / stats.total) * 100,
review: ((stats.review || 0) / stats.total) * 100
};
}
/**
* Format status counts in the correct order with colors
* @param stats - The statistics object containing counts
* @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
*/
function formatStatusLine(
stats: TaskStatistics,
isSubtask: boolean = false
): string {
const parts: string[] = [];
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
if (isSubtask) {
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
} else {
parts.push(`Done: ${chalk.green(stats.done)}`);
}
parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
// Add line break for second row
const firstLine = parts.join(' ');
parts.length = 0;
parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
const secondLine = parts.join(' ');
return firstLine + '\n' + secondLine;
}
/** /**
* Display the project dashboard box * Display the project dashboard box
*/ */
@@ -252,21 +407,36 @@ export function displayProjectDashboard(
subtaskStats: TaskStatistics, subtaskStats: TaskStatistics,
priorityBreakdown: Record<TaskPriority, number> priorityBreakdown: Record<TaskPriority, number>
): string { ): string {
const taskProgressBar = createProgressBar(taskStats.completionPercentage); // Calculate status breakdowns using the helper function
const subtaskProgressBar = createProgressBar(subtaskStats.completionPercentage); const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}%`; // Create progress bars with the breakdowns
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}%`; const taskProgressBar = createProgressBar(
taskStats.completionPercentage,
30,
taskStatusBreakdown
);
const subtaskProgressBar = createProgressBar(
subtaskStats.completionPercentage,
30,
subtaskStatusBreakdown
);
const content = const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
chalk.white.bold('Project Dashboard') + '\n' + const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
const content =
chalk.white.bold('Project Dashboard') +
'\n' +
`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` + `Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
`Done: ${chalk.green(taskStats.done)} In Progress: ${chalk.blue(taskStats.inProgress)} Pending: ${chalk.yellow(taskStats.pending)} Blocked: ${chalk.red(taskStats.blocked)} Deferred: ${chalk.gray(taskStats.deferred)}\n` + formatStatusLine(taskStats, false) +
`Cancelled: ${chalk.gray(taskStats.cancelled)}\n\n` + '\n\n' +
`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` + `Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
`Completed: ${chalk.green(`${subtaskStats.done}/${subtaskStats.total}`)} In Progress: ${chalk.blue(subtaskStats.inProgress)} Pending: ${chalk.yellow(subtaskStats.pending)} Blocked: ${chalk.red(subtaskStats.blocked)}\n` + formatStatusLine(subtaskStats, true) +
`Deferred: ${chalk.gray(subtaskStats.deferred)} Cancelled: ${chalk.gray(subtaskStats.cancelled)}\n\n` + '\n\n' +
chalk.cyan.bold('Priority Breakdown:') + '\n' + chalk.cyan.bold('Priority Breakdown:') +
'\n' +
`${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` + `${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` + `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`; `${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`;
@@ -281,24 +451,33 @@ export function displayDependencyDashboard(
depStats: DependencyStatistics, depStats: DependencyStatistics,
nextTask?: NextTaskInfo nextTask?: NextTaskInfo
): string { ): string {
const content = const content =
chalk.white.bold('Dependency Status & Next Task') + '\n' + chalk.white.bold('Dependency Status & Next Task') +
chalk.cyan.bold('Dependency Metrics:') + '\n' + '\n' +
chalk.cyan.bold('Dependency Metrics:') +
'\n' +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` + `${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` + `${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` + `${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${ `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${
depStats.mostDependedOnTaskId depStats.mostDependedOnTaskId
? chalk.cyan(`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`) ? chalk.cyan(
`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`
)
: chalk.gray('None') : chalk.gray('None')
}\n` + }\n` +
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` + `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold('Next Task to Work On:') + '\n' + chalk.cyan.bold('Next Task to Work On:') +
'\n' +
`ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${ `ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${
nextTask ? chalk.white.bold(nextTask.title) : chalk.yellow('No task available') nextTask
? chalk.white.bold(nextTask.title)
: chalk.yellow('No task available')
}\n` + }\n` +
`Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${ `Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${
nextTask?.dependencies?.length ? chalk.cyan(nextTask.dependencies.join(', ')) : chalk.gray('None') nextTask?.dependencies?.length
? chalk.cyan(nextTask.dependencies.join(', '))
: chalk.gray('None')
}\n` + }\n` +
`Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`;
@@ -315,8 +494,15 @@ export function displayDashboards(
depStats: DependencyStatistics, depStats: DependencyStatistics,
nextTask?: NextTaskInfo nextTask?: NextTaskInfo
): void { ): void {
const projectDashboardContent = displayProjectDashboard(taskStats, subtaskStats, priorityBreakdown); const projectDashboardContent = displayProjectDashboard(
const dependencyDashboardContent = displayDependencyDashboard(depStats, nextTask); taskStats,
subtaskStats,
priorityBreakdown
);
const dependencyDashboardContent = displayDependencyDashboard(
depStats,
nextTask
);
// Get terminal width // Get terminal width
const terminalWidth = process.stdout.columns || 80; const terminalWidth = process.stdout.columns || 80;
@@ -378,4 +564,4 @@ export function displayDashboards(
console.log(dashboardBox); console.log(dashboardBox);
console.log(dependencyBox); console.log(dependencyBox);
} }
} }

View File

@@ -7,6 +7,7 @@ import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import figlet from 'figlet'; import figlet from 'figlet';
import gradient from 'gradient-string'; import gradient from 'gradient-string';
import packageJson from '../../../package.json';
/** /**
* Header configuration options * Header configuration options
@@ -40,7 +41,7 @@ function createBanner(): string {
*/ */
export function displayHeader(options: HeaderOptions = {}): void { export function displayHeader(options: HeaderOptions = {}): void {
const { const {
version = '0.26.0', version = packageJson.version,
projectName = 'Taskmaster', projectName = 'Taskmaster',
tag, tag,
filePath, filePath,
@@ -50,7 +51,7 @@ export function displayHeader(options: HeaderOptions = {}): void {
// Display the ASCII banner if requested // Display the ASCII banner if requested
if (showBanner) { if (showBanner) {
console.log(createBanner()); console.log(createBanner());
// Add creator credit line below the banner // Add creator credit line below the banner
console.log( console.log(
chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano') chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano')
@@ -74,7 +75,7 @@ export function displayHeader(options: HeaderOptions = {}): void {
// Display tag and file path info // Display tag and file path info
if (tag || filePath) { if (tag || filePath) {
let tagInfo = ''; let tagInfo = '';
if (tag && tag !== 'master') { if (tag && tag !== 'master') {
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`; tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
} else { } else {
@@ -84,9 +85,11 @@ export function displayHeader(options: HeaderOptions = {}): void {
console.log(tagInfo); console.log(tagInfo);
if (filePath) { if (filePath) {
console.log( // Convert to absolute path if it's relative
`Listing tasks from: ${chalk.dim(filePath)}` const absolutePath = filePath.startsWith('/')
); ? filePath
: `${process.cwd()}/${filePath}`;
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
} }
console.log(); // Empty line for spacing console.log(); // Empty line for spacing
@@ -98,4 +101,4 @@ export function displayHeader(options: HeaderOptions = {}): void {
*/ */
export function displaySimpleHeader(options: HeaderOptions = {}): void { export function displaySimpleHeader(options: HeaderOptions = {}): void {
displayHeader({ ...options, showBanner: false }); displayHeader({ ...options, showBanner: false });
} }

View File

@@ -1,27 +1,31 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "ESNext",
"lib": ["ES2022"], "lib": ["ES2022"],
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowJs": false, "allowJs": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"types": ["node"] "types": ["node"],
}, "paths": {
"include": ["src/**/*"], "@tm/core": ["../../packages/tm-core/src/index.ts"],
"exclude": ["node_modules", "dist", "tests"] "@tm/core/*": ["../../packages/tm-core/src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
} }

View File

@@ -20,357 +20,8 @@
* Main entry point for globally installed package * Main entry point for globally installed package
*/ */
import { fileURLToPath } from 'url'; // Direct imports instead of spawning child processes
import { dirname, resolve } from 'path'; import { runCLI } from '../scripts/modules/commands.js';
import { createRequire } from 'module';
import { spawn } from 'child_process';
import { Command } from 'commander';
import { displayHelp, displayBanner } from '../scripts/modules/ui.js';
import { registerCommands } from '../scripts/modules/commands.js';
import { detectCamelCaseFlags } from '../scripts/modules/utils.js';
import chalk from 'chalk';
const __filename = fileURLToPath(import.meta.url); // Simply run the CLI directly
const __dirname = dirname(__filename); runCLI();
const require = createRequire(import.meta.url);
// Get package information
const packageJson = require('../package.json');
const version = packageJson.version;
// Get paths to script files
const devScriptPath = resolve(__dirname, '../scripts/dev.js');
const initScriptPath = resolve(__dirname, '../scripts/init.js');
// Helper function to run dev.js with arguments
function runDevScript(args) {
// Debug: Show the transformed arguments when DEBUG=1 is set
if (process.env.DEBUG === '1') {
console.error('\nDEBUG - CLI Wrapper Analysis:');
console.error('- Original command: ' + process.argv.join(' '));
console.error('- Transformed args: ' + args.join(' '));
console.error(
'- dev.js will receive: node ' +
devScriptPath +
' ' +
args.join(' ') +
'\n'
);
}
// For testing: If TEST_MODE is set, just print args and exit
if (process.env.TEST_MODE === '1') {
console.log('Would execute:');
console.log(`node ${devScriptPath} ${args.join(' ')}`);
process.exit(0);
return;
}
const child = spawn('node', [devScriptPath, ...args], {
stdio: 'inherit',
cwd: process.cwd()
});
child.on('close', (code) => {
process.exit(code);
});
}
// Helper function to detect camelCase and convert to kebab-case
const toKebabCase = (str) => str.replace(/([A-Z])/g, '-$1').toLowerCase();
/**
* Create a wrapper action that passes the command to dev.js
* @param {string} commandName - The name of the command
* @returns {Function} Wrapper action function
*/
function createDevScriptAction(commandName) {
return (options, cmd) => {
// Check for camelCase flags and error out with helpful message
const camelCaseFlags = detectCamelCaseFlags(process.argv);
// If camelCase flags were found, show error and exit
if (camelCaseFlags.length > 0) {
console.error('\nError: Please use kebab-case for CLI flags:');
camelCaseFlags.forEach((flag) => {
console.error(` Instead of: --${flag.original}`);
console.error(` Use: --${flag.kebabCase}`);
});
console.error(
'\nExample: task-master parse-prd --num-tasks=5 instead of --numTasks=5\n'
);
process.exit(1);
}
// Since we've ensured no camelCase flags, we can now just:
// 1. Start with the command name
const args = [commandName];
// 3. Get positional arguments and explicit flags from the command line
const commandArgs = [];
const positionals = new Set(); // Track positional args we've seen
// Find the command in raw process.argv to extract args
const commandIndex = process.argv.indexOf(commandName);
if (commandIndex !== -1) {
// Process all args after the command name
for (let i = commandIndex + 1; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg.startsWith('--')) {
// It's a flag - pass through as is
commandArgs.push(arg);
// Skip the next arg if this is a flag with a value (not --flag=value format)
if (
!arg.includes('=') &&
i + 1 < process.argv.length &&
!process.argv[i + 1].startsWith('--')
) {
commandArgs.push(process.argv[++i]);
}
} else if (!positionals.has(arg)) {
// It's a positional argument we haven't seen
commandArgs.push(arg);
positionals.add(arg);
}
}
}
// Add all command line args we collected
args.push(...commandArgs);
// 4. Add default options from Commander if not specified on command line
// Track which options we've seen on the command line
const userOptions = new Set();
for (const arg of commandArgs) {
if (arg.startsWith('--')) {
// Extract option name (without -- and value)
const name = arg.split('=')[0].slice(2);
userOptions.add(name);
// Add the kebab-case version too, to prevent duplicates
const kebabName = name.replace(/([A-Z])/g, '-$1').toLowerCase();
userOptions.add(kebabName);
// Add the camelCase version as well
const camelName = kebabName.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
userOptions.add(camelName);
}
}
// Add Commander-provided defaults for options not specified by user
Object.entries(options).forEach(([key, value]) => {
// Debug output to see what keys we're getting
if (process.env.DEBUG === '1') {
console.error(`DEBUG - Processing option: ${key} = ${value}`);
}
// Special case for numTasks > num-tasks (a known problem case)
if (key === 'numTasks') {
if (process.env.DEBUG === '1') {
console.error('DEBUG - Converting numTasks to num-tasks');
}
if (!userOptions.has('num-tasks') && !userOptions.has('numTasks')) {
args.push(`--num-tasks=${value}`);
}
return;
}
// Skip built-in Commander properties and options the user provided
if (
['parent', 'commands', 'options', 'rawArgs'].includes(key) ||
userOptions.has(key)
) {
return;
}
// Also check the kebab-case version of this key
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
if (userOptions.has(kebabKey)) {
return;
}
// Add default values, using kebab-case for the parameter name
if (value !== undefined) {
if (typeof value === 'boolean') {
if (value === true) {
args.push(`--${kebabKey}`);
} else if (value === false && key === 'generate') {
args.push('--skip-generate');
}
} else {
// Always use kebab-case for option names
args.push(`--${kebabKey}=${value}`);
}
}
});
// Special handling for parent parameter (uses -p)
if (options.parent && !args.includes('-p') && !userOptions.has('parent')) {
args.push('-p', options.parent);
}
// Debug output for troubleshooting
if (process.env.DEBUG === '1') {
console.error('DEBUG - Command args:', commandArgs);
console.error('DEBUG - User options:', Array.from(userOptions));
console.error('DEBUG - Commander options:', options);
console.error('DEBUG - Final args:', args);
}
// Run the script with our processed args
runDevScript(args);
};
}
// // Special case for the 'init' command which uses a different script
// function registerInitCommand(program) {
// program
// .command('init')
// .description('Initialize a new project')
// .option('-y, --yes', 'Skip prompts and use default values')
// .option('-n, --name <name>', 'Project name')
// .option('-d, --description <description>', 'Project description')
// .option('-v, --version <version>', 'Project version')
// .option('-a, --author <author>', 'Author name')
// .option('--skip-install', 'Skip installing dependencies')
// .option('--dry-run', 'Show what would be done without making changes')
// .action((options) => {
// // Pass through any options to the init script
// const args = [
// '--yes',
// 'name',
// 'description',
// 'version',
// 'author',
// 'skip-install',
// 'dry-run'
// ]
// .filter((opt) => options[opt])
// .map((opt) => {
// if (opt === 'yes' || opt === 'skip-install' || opt === 'dry-run') {
// return `--${opt}`;
// }
// return `--${opt}=${options[opt]}`;
// });
// const child = spawn('node', [initScriptPath, ...args], {
// stdio: 'inherit',
// cwd: process.cwd()
// });
// child.on('close', (code) => {
// process.exit(code);
// });
// });
// }
// Set up the command-line interface
const program = new Command();
program
.name('task-master')
.description('Claude Task Master CLI')
.version(version)
.addHelpText('afterAll', () => {
// Use the same help display function as dev.js for consistency
displayHelp();
return ''; // Return empty string to prevent commander's default help
});
// Add custom help option to directly call our help display
program.helpOption('-h, --help', 'Display help information');
program.on('--help', () => {
displayHelp();
});
// // Add special case commands
// registerInitCommand(program);
program
.command('dev')
.description('Run the dev.js script')
.action(() => {
const args = process.argv.slice(process.argv.indexOf('dev') + 1);
runDevScript(args);
});
// Use a temporary Command instance to get all command definitions
const tempProgram = new Command();
registerCommands(tempProgram);
// For each command in the temp instance, add a modified version to our actual program
tempProgram.commands.forEach((cmd) => {
if (['dev'].includes(cmd.name())) {
// Skip commands we've already defined specially
return;
}
// Create a new command with the same name and description
const newCmd = program.command(cmd.name()).description(cmd.description());
// Copy all options
cmd.options.forEach((opt) => {
newCmd.option(opt.flags, opt.description, opt.defaultValue);
});
// Set the action to proxy to dev.js
newCmd.action(createDevScriptAction(cmd.name()));
});
// Parse the command line arguments
program.parse(process.argv);
// Add global error handling for unknown commands and options
process.on('uncaughtException', (err) => {
// Check if this is a commander.js unknown option error
if (err.code === 'commander.unknownOption') {
const option = err.message.match(/'([^']+)'/)?.[1];
const commandArg = process.argv.find(
(arg) =>
!arg.startsWith('-') &&
arg !== 'task-master' &&
!arg.includes('/') &&
arg !== 'node'
);
const command = commandArg || 'unknown';
console.error(chalk.red(`Error: Unknown option '${option}'`));
console.error(
chalk.yellow(
`Run 'task-master ${command} --help' to see available options for this command`
)
);
process.exit(1);
}
// Check if this is a commander.js unknown command error
if (err.code === 'commander.unknownCommand') {
const command = err.message.match(/'([^']+)'/)?.[1];
console.error(chalk.red(`Error: Unknown command '${command}'`));
console.error(
chalk.yellow(`Run 'task-master --help' to see available commands`)
);
process.exit(1);
}
// Handle other uncaught exceptions
console.error(chalk.red(`Error: ${err.message}`));
if (process.env.DEBUG === '1') {
console.error(err);
}
process.exit(1);
});
// Show help if no command was provided (just 'task-master' with no args)
if (process.argv.length <= 2) {
displayBanner();
displayHelp();
process.exit(0);
}
// Add exports at the end of the file
export { detectCamelCaseFlags };

View File

@@ -7,50 +7,17 @@
"types": "./src/index.ts", "types": "./src/index.ts",
"main": "./dist/index.js", "main": "./dist/index.js",
"exports": { "exports": {
".": { ".": "./src/index.ts",
"types": "./src/index.ts", "./auth": "./src/auth/index.ts",
"import": "./dist/index.js" "./storage": "./src/storage/index.ts",
}, "./config": "./src/config/index.ts",
"./auth": { "./providers": "./src/providers/index.ts",
"types": "./src/auth/index.ts", "./services": "./src/services/index.ts",
"import": "./dist/auth/index.js" "./errors": "./src/errors/index.ts",
}, "./logger": "./src/logger/index.ts",
"./storage": { "./types": "./src/types/index.ts",
"types": "./src/storage/index.ts", "./interfaces": "./src/interfaces/index.ts",
"import": "./dist/storage/index.js" "./utils": "./src/utils/index.ts"
},
"./config": {
"types": "./src/config/index.ts",
"import": "./dist/config/index.js"
},
"./providers": {
"types": "./src/providers/index.ts",
"import": "./dist/providers/index.js"
},
"./services": {
"types": "./src/services/index.ts",
"import": "./dist/services/index.js"
},
"./errors": {
"types": "./src/errors/index.ts",
"import": "./dist/errors/index.js"
},
"./logger": {
"types": "./src/logger/index.ts",
"import": "./dist/logger/index.js"
},
"./types": {
"types": "./src/types/index.ts",
"import": "./dist/types/index.js"
},
"./interfaces": {
"types": "./src/interfaces/index.ts",
"import": "./dist/interfaces/index.js"
},
"./utils": {
"types": "./src/utils/index.ts",
"import": "./dist/utils/index.js"
}
}, },
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",

View File

@@ -1,6 +1,10 @@
import { defineConfig } from 'tsup'; import { defineConfig } from 'tsup';
import { baseConfig, mergeConfig } from '@tm/build-config'; import { baseConfig, mergeConfig } from '@tm/build-config';
import { load as dotenvLoad } from 'dotenv-mono'; import { load as dotenvLoad } from 'dotenv-mono';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenvLoad(); dotenvLoad();
@@ -16,28 +20,38 @@ const getBuildTimeEnvs = () => {
return envs; return envs;
}; };
export default defineConfig( export default defineConfig(
mergeConfig(baseConfig, { mergeConfig(baseConfig, {
entry: { entry: {
'task-master': 'bin/task-master.js', 'task-master': 'scripts/dev.js',
'mcp-server': 'mcp-server/server.js' 'mcp-server': 'mcp-server/server.js'
}, },
outDir: 'dist', outDir: 'dist',
publicDir: 'public', publicDir: 'public',
// Bundle our monorepo packages but keep node_modules external // Override the base config's external to bundle our workspace packages
noExternal: [/@tm\/.*/], noExternal: [/^@tm\//],
// Ensure no code splitting external: [/^@supabase\//], // Keep Supabase external to avoid dynamic require issues
splitting: false, env: getBuildTimeEnvs(),
// Better watch configuration esbuildOptions(options) {
ignoreWatch: [ // Set up path aliases for workspace packages
'dist', options.alias = {
'node_modules', '@tm/core': path.resolve(__dirname, 'packages/tm-core/src/index.ts'),
'.git', '@tm/core/auth': path.resolve(__dirname, 'packages/tm-core/src/auth/index.ts'),
'tests', '@tm/core/storage': path.resolve(__dirname, 'packages/tm-core/src/storage/index.ts'),
'*.test.*', '@tm/core/config': path.resolve(__dirname, 'packages/tm-core/src/config/index.ts'),
'*.spec.*' '@tm/core/providers': path.resolve(__dirname, 'packages/tm-core/src/providers/index.ts'),
], '@tm/core/services': path.resolve(__dirname, 'packages/tm-core/src/services/index.ts'),
env: getBuildTimeEnvs() '@tm/core/errors': path.resolve(__dirname, 'packages/tm-core/src/errors/index.ts'),
'@tm/core/logger': path.resolve(__dirname, 'packages/tm-core/src/logger/index.ts'),
'@tm/core/types': path.resolve(__dirname, 'packages/tm-core/src/types/index.ts'),
'@tm/core/interfaces': path.resolve(__dirname, 'packages/tm-core/src/interfaces/index.ts'),
'@tm/core/utils': path.resolve(__dirname, 'packages/tm-core/src/utils/index.ts'),
'@tm/cli': path.resolve(__dirname, 'apps/cli/src/index.ts'),
'@tm/cli/commands': path.resolve(__dirname, 'apps/cli/src/commands/index.ts'),
'@tm/cli/utils': path.resolve(__dirname, 'apps/cli/src/utils/index.ts'),
'@tm/cli/ui': path.resolve(__dirname, 'apps/cli/src/ui/index.ts'),
'@tm/build-config': path.resolve(__dirname, 'packages/build-config/src/tsup.base.ts'),
};
}
}) })
); );