diff --git a/apps/cli/package.json b/apps/cli/package.json index 3e030acf..82d47dda 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,15 +6,10 @@ "main": "./dist/index.js", "types": "./src/index.ts", "exports": { - ".": { - "types": "./src/index.ts", - "import": "./dist/index.js" - } + ".": "./src/index.ts" }, "files": ["dist", "README.md"], "scripts": { - "build": "tsc", - "dev": "tsc --watch", "typecheck": "tsc --noEmit", "lint": "biome check src", "format": "biome format --write src", @@ -48,5 +43,10 @@ }, "keywords": ["task-master", "cli", "task-management", "productivity"], "author": "", - "license": "MIT" + "license": "MIT", + "typesVersions": { + "*": { + "*": ["src/*"] + } + } } diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 7105daca..57adf824 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -17,6 +17,18 @@ import { } from '@tm/core'; import type { StorageType } from '@tm/core/types'; import * as ui from '../utils/ui.js'; +import { + displayHeader, + displayDashboards, + calculateTaskStatistics, + calculateSubtaskStatistics, + calculateDependencyStatistics, + getPriorityBreakdown, + displayRecommendedNextTask, + getTaskDescription, + displaySuggestedNextSteps, + type NextTaskInfo +} from '../ui/index.js'; /** * Options interface for the list command @@ -245,19 +257,16 @@ export class ListTasksCommand extends Command { * Display in text format with tables */ private displayText(data: ListTasksResult, withSubtasks?: boolean): void { - const { tasks, total, filtered, tag, storageType } = data; + const { tasks, tag } = data; - // Header - ui.displayBanner(`Task List${tag ? ` (${tag})` : ''}`); + // Get file path for display + const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined; - // Statistics - console.log(chalk.blue.bold('\nšŸ“Š Statistics:\n')); - console.log(` Total tasks: ${chalk.cyan(total)}`); - console.log(` Filtered: ${chalk.cyan(filtered)}`); - if (tag) { - console.log(` Tag: ${chalk.cyan(tag)}`); - } - console.log(` Storage: ${chalk.cyan(storageType)}`); + // Display header without banner (banner already shown by main CLI) + displayHeader({ + tag: tag || 'master', + filePath: filePath + }); // No tasks message if (tasks.length === 0) { @@ -265,21 +274,50 @@ export class ListTasksCommand extends Command { return; } - // Task table - console.log(chalk.blue.bold(`\nšŸ“‹ Tasks (${tasks.length}):\n`)); + // Calculate statistics + const taskStats = calculateTaskStatistics(tasks); + const subtaskStats = calculateSubtaskStatistics(tasks); + const depStats = calculateDependencyStatistics(tasks); + const priorityBreakdown = getPriorityBreakdown(tasks); + + // Find next task following the same logic as findNextTask + const nextTask = this.findNextTask(tasks); + + // Display dashboard boxes + displayDashboards( + taskStats, + subtaskStats, + priorityBreakdown, + depStats, + nextTask + ); + + // Task table - no title, just show the table directly console.log( ui.createTaskTable(tasks, { showSubtasks: withSubtasks, - showDependencies: true + showDependencies: true, + showComplexity: true // Enable complexity column }) ); - // Progress bar - const completedCount = tasks.filter( - (t: Task) => t.status === 'done' - ).length; - console.log(chalk.blue.bold('\nšŸ“Š Overall Progress:\n')); - console.log(` ${ui.createProgressBar(completedCount, tasks.length)}`); + // Display recommended next task section immediately after table + if (nextTask) { + // Find the full task object to get description + const fullTask = tasks.find((t) => String(t.id) === String(nextTask.id)); + const description = fullTask ? getTaskDescription(fullTask) : undefined; + + displayRecommendedNextTask({ + ...nextTask, + status: 'pending', // Next task is typically pending + description + }); + } else { + displayRecommendedNextTask(undefined); + } + + // Display suggested next steps at the end + displaySuggestedNextSteps(); } /** @@ -289,6 +327,128 @@ export class ListTasksCommand extends Command { 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 = { + critical: 4, + high: 3, + medium: 2, + low: 1 + }; + + // Build set of completed task IDs (including subtasks) + const completedIds = new Set(); + 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) */ diff --git a/apps/cli/src/commands/show.command.ts b/apps/cli/src/commands/show.command.ts index d9bace90..14ebbba1 100644 --- a/apps/cli/src/commands/show.command.ts +++ b/apps/cli/src/commands/show.command.ts @@ -9,6 +9,14 @@ import boxen from 'boxen'; import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; import type { StorageType } from '@tm/core/types'; import * as ui from '../utils/ui.js'; +import { + displayTaskHeader, + displayTaskProperties, + displayImplementationDetails, + displayTestStrategy, + displaySubtasks, + displaySuggestedActions +} from '../ui/components/task-detail.component.js'; /** * Options interface for the show command @@ -258,46 +266,26 @@ export class ShowCommand extends Command { const task = result.task; - // Header - console.log( - boxen(chalk.white.bold(`Task #${task.id} - ${task.title}`), { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1 } - }) - ); + // Display header with tag + displayTaskHeader(task.id, task.title); - // Task details - console.log( - `\n${chalk.blue.bold('Status:')} ${ui.getStatusWithColor(task.status)}` - ); - console.log( - `${chalk.blue.bold('Priority:')} ${ui.getPriorityWithColor(task.priority)}` - ); - - if (task.description) { - console.log(`\n${chalk.blue.bold('Description:')}`); - console.log(task.description); - } + // Display task properties in table format + displayTaskProperties(task); + // Display implementation details if available if (task.details) { - console.log(`\n${chalk.blue.bold('Details:')}`); - console.log(task.details); + console.log(); // Empty line for spacing + displayImplementationDetails(task.details); } - // Dependencies - if (task.dependencies && task.dependencies.length > 0) { - console.log(`\n${chalk.blue.bold('Dependencies:')}`); - task.dependencies.forEach((dep) => { - console.log(` - ${chalk.cyan(dep)}`); - }); + // Display test strategy if available + if ('testStrategy' in task && task.testStrategy) { + console.log(); // Empty line for spacing + displayTestStrategy(task.testStrategy as string); } - // Subtasks + // Display subtasks if available if (task.subtasks && task.subtasks.length > 0) { - console.log(`\n${chalk.blue.bold('Subtasks:')}`); - // Filter subtasks by status if provided const filteredSubtasks = options.status ? task.subtasks.filter((sub) => sub.status === options.status) @@ -308,23 +296,12 @@ export class ShowCommand extends Command { chalk.gray(` No subtasks with status '${options.status}'`) ); } else { - filteredSubtasks.forEach((subtask) => { - console.log( - ` ${chalk.cyan(`${task.id}.${subtask.id}`)} ${ui.getStatusWithColor(subtask.status)} ${subtask.title}` - ); - if (subtask.description) { - console.log(` ${chalk.gray(subtask.description)}`); - } - }); + displaySubtasks(filteredSubtasks, task.id); } } - if (task.testStrategy) { - console.log(`\n${chalk.blue.bold('Test Strategy:')}`); - console.log(task.testStrategy); - } - - console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`); + // Display suggested actions + displaySuggestedActions(task.id); } /** diff --git a/apps/cli/src/ui/components/dashboard.component.ts b/apps/cli/src/ui/components/dashboard.component.ts new file mode 100644 index 00000000..56ffeb70 --- /dev/null +++ b/apps/cli/src/ui/components/dashboard.component.ts @@ -0,0 +1,567 @@ +/** + * @fileoverview Dashboard components for Task Master CLI + * Displays project statistics and dependency information + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; +import type { Task, TaskPriority } from '@tm/core/types'; + +/** + * Statistics for task collection + */ +export interface TaskStatistics { + total: number; + done: number; + inProgress: number; + pending: number; + blocked: number; + deferred: number; + cancelled: number; + review?: number; + completionPercentage: number; +} + +/** + * Statistics for dependencies + */ +export interface DependencyStatistics { + tasksWithNoDeps: number; + tasksReadyToWork: number; + tasksBlockedByDeps: number; + mostDependedOnTaskId?: number; + mostDependedOnCount?: number; + avgDependenciesPerTask: number; +} + +/** + * Next task information + */ +export interface NextTaskInfo { + id: string | number; + title: string; + priority?: TaskPriority; + dependencies?: (string | number)[]; + complexity?: number | string; +} + +/** + * Status breakdown for progress bars + */ +export interface StatusBreakdown { + 'in-progress'?: number; + pending?: number; + blocked?: number; + 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; +} + +/** + * Calculate task statistics from a list of tasks + */ +export function calculateTaskStatistics(tasks: Task[]): TaskStatistics { + const stats: TaskStatistics = { + total: tasks.length, + done: 0, + inProgress: 0, + pending: 0, + blocked: 0, + deferred: 0, + cancelled: 0, + review: 0, + completionPercentage: 0 + }; + + tasks.forEach((task) => { + switch (task.status) { + case 'done': + stats.done++; + break; + case 'in-progress': + stats.inProgress++; + break; + case 'pending': + stats.pending++; + break; + case 'blocked': + stats.blocked++; + break; + case 'deferred': + stats.deferred++; + break; + case 'cancelled': + stats.cancelled++; + break; + case 'review': + stats.review = (stats.review || 0) + 1; + break; + } + }); + + stats.completionPercentage = + stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; + + return stats; +} + +/** + * Calculate subtask statistics from tasks + */ +export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { + const stats: TaskStatistics = { + total: 0, + done: 0, + inProgress: 0, + pending: 0, + blocked: 0, + deferred: 0, + cancelled: 0, + review: 0, + completionPercentage: 0 + }; + + tasks.forEach((task) => { + if (task.subtasks && task.subtasks.length > 0) { + task.subtasks.forEach((subtask) => { + stats.total++; + switch (subtask.status) { + case 'done': + stats.done++; + break; + case 'in-progress': + stats.inProgress++; + break; + case 'pending': + stats.pending++; + break; + case 'blocked': + stats.blocked++; + break; + case 'deferred': + stats.deferred++; + break; + case 'cancelled': + stats.cancelled++; + break; + case 'review': + stats.review = (stats.review || 0) + 1; + break; + } + }); + } + }); + + stats.completionPercentage = + stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; + + return stats; +} + +/** + * Calculate dependency statistics + */ +export function calculateDependencyStatistics( + tasks: Task[] +): DependencyStatistics { + const completedTaskIds = new Set( + tasks.filter((t) => t.status === 'done').map((t) => t.id) + ); + + const tasksWithNoDeps = tasks.filter( + (t) => + t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) + ).length; + + const tasksWithAllDepsSatisfied = tasks.filter( + (t) => + t.status !== 'done' && + t.dependencies && + t.dependencies.length > 0 && + t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; + + const tasksBlockedByDeps = tasks.filter( + (t) => + t.status !== 'done' && + t.dependencies && + t.dependencies.length > 0 && + !t.dependencies.every((depId) => completedTaskIds.has(depId)) + ).length; + + // Calculate most depended-on task + const dependencyCount: Record = {}; + tasks.forEach((task) => { + if (task.dependencies && task.dependencies.length > 0) { + task.dependencies.forEach((depId) => { + const key = String(depId); + dependencyCount[key] = (dependencyCount[key] || 0) + 1; + }); + } + }); + + let mostDependedOnTaskId: number | undefined; + let mostDependedOnCount = 0; + + for (const [taskId, count] of Object.entries(dependencyCount)) { + if (count > mostDependedOnCount) { + mostDependedOnCount = count; + mostDependedOnTaskId = parseInt(taskId); + } + } + + // Calculate average dependencies + const totalDependencies = tasks.reduce( + (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), + 0 + ); + const avgDependenciesPerTask = + tasks.length > 0 ? totalDependencies / tasks.length : 0; + + return { + tasksWithNoDeps, + tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied, + tasksBlockedByDeps, + mostDependedOnTaskId, + mostDependedOnCount, + avgDependenciesPerTask + }; +} + +/** + * Get priority counts + */ +export function getPriorityBreakdown( + tasks: Task[] +): Record { + const breakdown: Record = { + critical: 0, + high: 0, + medium: 0, + low: 0 + }; + + tasks.forEach((task) => { + const priority = task.priority || 'medium'; + breakdown[priority]++; + }); + + 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 + */ +export function displayProjectDashboard( + taskStats: TaskStatistics, + subtaskStats: TaskStatistics, + priorityBreakdown: Record +): string { + // Calculate status breakdowns using the helper function + const taskStatusBreakdown = calculateStatusBreakdown(taskStats); + const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats); + + // Create progress bars with the breakdowns + const taskProgressBar = createProgressBar( + taskStats.completionPercentage, + 30, + taskStatusBreakdown + ); + const subtaskProgressBar = createProgressBar( + subtaskStats.completionPercentage, + 30, + subtaskStatusBreakdown + ); + + const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`; + const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`; + + const content = + chalk.white.bold('Project Dashboard') + + '\n' + + `Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` + + formatStatusLine(taskStats, false) + + '\n\n' + + `Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` + + formatStatusLine(subtaskStats, true) + + '\n\n' + + chalk.cyan.bold('Priority Breakdown:') + + '\n' + + `${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` + + `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` + + `${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`; + + return content; +} + +/** + * Display the dependency dashboard box + */ +export function displayDependencyDashboard( + depStats: DependencyStatistics, + nextTask?: NextTaskInfo +): string { + const content = + chalk.white.bold('Dependency Status & Next Task') + + '\n' + + chalk.cyan.bold('Dependency Metrics:') + + '\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.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` + + `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${ + depStats.mostDependedOnTaskId + ? chalk.cyan( + `#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)` + ) + : chalk.gray('None') + }\n` + + `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` + + chalk.cyan.bold('Next Task to Work On:') + + '\n' + + `ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${ + nextTask + ? chalk.white.bold(nextTask.title) + : chalk.yellow('No task available') + }\n` + + `Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${ + nextTask?.dependencies?.length + ? chalk.cyan(nextTask.dependencies.join(', ')) + : chalk.gray('None') + }\n` + + `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; + + return content; +} + +/** + * Display dashboard boxes side by side or stacked + */ +export function displayDashboards( + taskStats: TaskStatistics, + subtaskStats: TaskStatistics, + priorityBreakdown: Record, + depStats: DependencyStatistics, + nextTask?: NextTaskInfo +): void { + const projectDashboardContent = displayProjectDashboard( + taskStats, + subtaskStats, + priorityBreakdown + ); + const dependencyDashboardContent = displayDependencyDashboard( + depStats, + nextTask + ); + + // Get terminal width + const terminalWidth = process.stdout.columns || 80; + const minDashboardWidth = 50; + const minDependencyWidth = 50; + const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; + + // If terminal is wide enough, show side by side + if (terminalWidth >= totalMinWidth) { + const halfWidth = Math.floor(terminalWidth / 2); + const boxContentWidth = halfWidth - 4; + + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); + + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); + + // Create side-by-side layout + const dashboardLines = dashboardBox.split('\n'); + const dependencyLines = dependencyBox.split('\n'); + const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); + + const combinedLines = []; + for (let i = 0; i < maxHeight; i++) { + const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; + const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; + const paddedDashLine = dashLine.padEnd(halfWidth, ' '); + combinedLines.push(paddedDashLine + depLine); + } + + console.log(combinedLines.join('\n')); + } else { + // Show stacked vertically + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); + + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); + + console.log(dashboardBox); + console.log(dependencyBox); + } +} diff --git a/apps/cli/src/ui/components/header.component.ts b/apps/cli/src/ui/components/header.component.ts new file mode 100644 index 00000000..c4dacb58 --- /dev/null +++ b/apps/cli/src/ui/components/header.component.ts @@ -0,0 +1,47 @@ +/** + * @fileoverview Task Master header component + * Displays the banner, version, project info, and file path + */ + +import chalk from 'chalk'; +import figlet from 'figlet'; +import gradient from 'gradient-string'; + +/** + * Header configuration options + */ +export interface HeaderOptions { + title?: string; + tag?: string; + filePath?: string; +} + +/** + * Display the Task Master header with project info + */ +export function displayHeader(options: HeaderOptions = {}): void { + const { filePath, tag } = options; + + // Display tag and file path info + if (tag) { + let tagInfo = ''; + + if (tag && tag !== 'master') { + tagInfo = `šŸ· tag: ${chalk.cyan(tag)}`; + } else { + tagInfo = `šŸ· tag: ${chalk.cyan('master')}`; + } + + console.log(tagInfo); + + if (filePath) { + // Convert to absolute path if it's relative + const absolutePath = filePath.startsWith('/') + ? filePath + : `${process.cwd()}/${filePath}`; + console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`); + } + + console.log(); // Empty line for spacing + } +} diff --git a/apps/cli/src/ui/components/index.ts b/apps/cli/src/ui/components/index.ts new file mode 100644 index 00000000..b5efbcda --- /dev/null +++ b/apps/cli/src/ui/components/index.ts @@ -0,0 +1,9 @@ +/** + * @fileoverview UI components exports + */ + +export * from './header.component.js'; +export * from './dashboard.component.js'; +export * from './next-task.component.js'; +export * from './suggested-steps.component.js'; +export * from './task-detail.component.js'; diff --git a/apps/cli/src/ui/components/next-task.component.ts b/apps/cli/src/ui/components/next-task.component.ts new file mode 100644 index 00000000..beb5d7cc --- /dev/null +++ b/apps/cli/src/ui/components/next-task.component.ts @@ -0,0 +1,134 @@ +/** + * @fileoverview Next task recommendation component + * Displays detailed information about the recommended next task + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; +import type { Task } from '@tm/core/types'; + +/** + * Next task display options + */ +export interface NextTaskDisplayOptions { + id: string | number; + title: string; + priority?: string; + status?: string; + dependencies?: (string | number)[]; + description?: string; +} + +/** + * Display the recommended next task section + */ +export function displayRecommendedNextTask( + task: NextTaskDisplayOptions | undefined +): void { + if (!task) { + // If no task available, show a message + console.log( + boxen( + chalk.yellow( + 'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.' + ), + { + padding: 1, + borderStyle: 'round', + borderColor: 'yellow', + title: '⚠ NO TASKS AVAILABLE ⚠', + titleAlignment: 'center' + } + ) + ); + return; + } + + // Build the content for the next task box + const content = []; + + // Task header with ID and title + content.push( + `šŸ”„ ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}` + ); + content.push(''); + + // Priority and Status line + const statusLine = []; + if (task.priority) { + const priorityColor = + task.priority === 'high' + ? chalk.red + : task.priority === 'medium' + ? chalk.yellow + : chalk.gray; + statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`); + } + if (task.status) { + const statusDisplay = + task.status === 'pending' + ? chalk.yellow('ā—‹ pending') + : task.status === 'in-progress' + ? chalk.blue('ā–¶ in-progress') + : chalk.gray(task.status); + statusLine.push(`Status: ${statusDisplay}`); + } + content.push(statusLine.join(' ')); + + // Dependencies + const depsDisplay = + !task.dependencies || task.dependencies.length === 0 + ? chalk.gray('None') + : chalk.cyan(task.dependencies.join(', ')); + content.push(`Dependencies: ${depsDisplay}`); + + // Description if available + if (task.description) { + content.push(''); + content.push(`Description: ${chalk.white(task.description)}`); + } + + // Action commands + content.push(''); + content.push( + `${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}` + ); + content.push( + `${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}` + ); + + // Display in a styled box with orange border + console.log( + boxen(content.join('\n'), { + padding: 1, + margin: { top: 1, bottom: 1 }, + borderStyle: 'round', + borderColor: '#FFA500', // Orange color + title: chalk.hex('#FFA500')('⚔ RECOMMENDED NEXT TASK ⚔'), + titleAlignment: 'center', + width: process.stdout.columns * 0.97, + fullscreen: false + }) + ); +} + +/** + * Get task description from the full task object + */ +export function getTaskDescription(task: Task): string | undefined { + // Try to get description from the task + // This could be from task.description or the first line of task.details + if ('description' in task && task.description) { + return task.description as string; + } + + if ('details' in task && task.details) { + // Take first sentence or line from details + const details = task.details as string; + const firstLine = details.split('\n')[0]; + const firstSentence = firstLine.split('.')[0]; + return firstSentence; + } + + return undefined; +} diff --git a/apps/cli/src/ui/components/suggested-steps.component.ts b/apps/cli/src/ui/components/suggested-steps.component.ts new file mode 100644 index 00000000..66e5eb19 --- /dev/null +++ b/apps/cli/src/ui/components/suggested-steps.component.ts @@ -0,0 +1,31 @@ +/** + * @fileoverview Suggested next steps component + * Displays helpful command suggestions at the end of the list + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; + +/** + * Display suggested next steps section + */ +export function displaySuggestedNextSteps(): void { + const steps = [ + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`, + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=')} to break down a task into subtasks`, + `${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id= --status=done')} to mark a task as complete` + ]; + + console.log( + boxen( + chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'), + { + padding: 1, + margin: { top: 0, bottom: 1 }, + borderStyle: 'round', + borderColor: 'gray', + width: process.stdout.columns * 0.97 + } + ) + ); +} diff --git a/apps/cli/src/ui/components/task-detail.component.ts b/apps/cli/src/ui/components/task-detail.component.ts new file mode 100644 index 00000000..7f1f1d20 --- /dev/null +++ b/apps/cli/src/ui/components/task-detail.component.ts @@ -0,0 +1,264 @@ +/** + * @fileoverview Task detail component for show command + * Displays detailed task information in a structured format + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; +import Table from 'cli-table3'; +import { marked, MarkedExtension } from 'marked'; +import { markedTerminal } from 'marked-terminal'; +import type { Task } from '@tm/core/types'; +import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js'; + +// Configure marked to use terminal renderer with subtle colors +marked.use( + markedTerminal({ + // More subtle colors that match the overall design + code: (code: string) => { + // Custom code block handler to preserve formatting + return code + .split('\n') + .map((line) => ' ' + chalk.cyan(line)) + .join('\n'); + }, + blockquote: chalk.gray.italic, + html: chalk.gray, + heading: chalk.white.bold, // White bold for headings + hr: chalk.gray, + listitem: chalk.white, // White for list items + paragraph: chalk.white, // White for paragraphs (default text color) + strong: chalk.white.bold, // White bold for strong text + em: chalk.white.italic, // White italic for emphasis + codespan: chalk.cyan, // Cyan for inline code (no background) + del: chalk.dim.strikethrough, + link: chalk.blue, + href: chalk.blue.underline, + // Add more explicit code block handling + showSectionPrefix: false, + unescape: true, + emoji: false, + // Try to preserve whitespace in code blocks + tab: 4, + width: 120 + }) as MarkedExtension +); + +// Also set marked options to preserve whitespace +marked.setOptions({ + breaks: true, + gfm: true +}); + +/** + * Display the task header with tag + */ +export function displayTaskHeader( + taskId: string | number, + title: string +): void { + // Display task header box + console.log( + boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: 'blue', + borderStyle: 'round' + }) + ); +} + +/** + * Display task properties in a table format + */ +export function displayTaskProperties(task: Task): void { + const terminalWidth = process.stdout.columns * 0.95 || 100; + // Create table for task properties - simple 2-column layout + const table = new Table({ + head: [], + style: { + head: [], + border: ['grey'] + }, + colWidths: [ + Math.floor(terminalWidth * 0.2), + Math.floor(terminalWidth * 0.8) + ], + wordWrap: true + }); + + const deps = + task.dependencies && task.dependencies.length > 0 + ? task.dependencies.map((d) => String(d)).join(', ') + : 'None'; + + // Build the left column (labels) and right column (values) + const labels = [ + chalk.cyan('ID:'), + chalk.cyan('Title:'), + chalk.cyan('Status:'), + chalk.cyan('Priority:'), + chalk.cyan('Dependencies:'), + chalk.cyan('Complexity:'), + chalk.cyan('Description:') + ].join('\n'); + + const values = [ + String(task.id), + task.title, + getStatusWithColor(task.status), + getPriorityWithColor(task.priority), + deps, + 'N/A', + task.description || '' + ].join('\n'); + + table.push([labels, values]); + + console.log(table.toString()); +} + +/** + * Display implementation details in a box + */ +export function displayImplementationDetails(details: string): void { + // Handle all escaped characters properly + const cleanDetails = details + .replace(/\\n/g, '\n') // Convert \n to actual newlines + .replace(/\\t/g, '\t') // Convert \t to actual tabs + .replace(/\\"/g, '"') // Convert \" to actual quotes + .replace(/\\\\/g, '\\'); // Convert \\ to single backslash + + const terminalWidth = process.stdout.columns * 0.95 || 100; + + // Parse markdown to terminal-friendly format + const markdownResult = marked(cleanDetails); + const formattedDetails = + typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise + + console.log( + boxen( + chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails, + { + padding: 1, + borderStyle: 'round', + borderColor: 'cyan', // Changed to cyan to match the original + width: terminalWidth // Fixed width to match the original + } + ) + ); +} + +/** + * Display test strategy in a box + */ +export function displayTestStrategy(testStrategy: string): void { + // Handle all escaped characters properly (same as implementation details) + const cleanStrategy = testStrategy + .replace(/\\n/g, '\n') // Convert \n to actual newlines + .replace(/\\t/g, '\t') // Convert \t to actual tabs + .replace(/\\"/g, '"') // Convert \" to actual quotes + .replace(/\\\\/g, '\\'); // Convert \\ to single backslash + + const terminalWidth = process.stdout.columns * 0.95 || 100; + + // Parse markdown to terminal-friendly format (same as implementation details) + const markdownResult = marked(cleanStrategy); + const formattedStrategy = + typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise + + console.log( + boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, { + padding: 1, + borderStyle: 'round', + borderColor: 'cyan', // Changed to cyan to match implementation details + width: terminalWidth + }) + ); +} + +/** + * Display subtasks in a table format + */ +export function displaySubtasks( + subtasks: Array<{ + id: string | number; + title: string; + status: any; + description?: string; + dependencies?: string[]; + }>, + parentId: string | number +): void { + const terminalWidth = process.stdout.columns * 0.95 || 100; + // Display subtasks header + console.log( + boxen(chalk.magenta.bold('Subtasks'), { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: 'magenta', + borderStyle: 'round', + margin: { top: 1, bottom: 0 } + }) + ); + + // Create subtasks table + const table = new Table({ + head: [ + chalk.magenta.bold('ID'), + chalk.magenta.bold('Status'), + chalk.magenta.bold('Title'), + chalk.magenta.bold('Deps') + ], + style: { + head: [], + border: ['grey'] + }, + colWidths: [ + Math.floor(terminalWidth * 0.1), + Math.floor(terminalWidth * 0.15), + Math.floor(terminalWidth * 0.6), + Math.floor(terminalWidth * 0.15) + ], + wordWrap: true + }); + + subtasks.forEach((subtask) => { + const subtaskId = `${parentId}.${subtask.id}`; + + // Format dependencies + const deps = + subtask.dependencies && subtask.dependencies.length > 0 + ? subtask.dependencies.join(', ') + : 'None'; + + table.push([ + subtaskId, + getStatusWithColor(subtask.status), + subtask.title, + deps + ]); + }); + + console.log(table.toString()); +} + +/** + * Display suggested actions + */ +export function displaySuggestedActions(taskId: string | number): void { + console.log( + boxen( + chalk.white.bold('Suggested Actions:') + + '\n\n' + + `${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` + + `${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` + + `${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`, + { + padding: 1, + margin: { top: 1 }, + borderStyle: 'round', + borderColor: 'green', + width: process.stdout.columns * 0.95 || 100 + } + ) + ); +} diff --git a/apps/cli/src/ui/index.ts b/apps/cli/src/ui/index.ts new file mode 100644 index 00000000..f735fa4b --- /dev/null +++ b/apps/cli/src/ui/index.ts @@ -0,0 +1,9 @@ +/** + * @fileoverview Main UI exports + */ + +// Export all components +export * from './components/index.js'; + +// Re-export existing UI utilities +export * from '../utils/ui.js'; diff --git a/apps/cli/src/utils/ui.ts b/apps/cli/src/utils/ui.ts index 4912a519..7ddbd34e 100644 --- a/apps/cli/src/utils/ui.ts +++ b/apps/cli/src/utils/ui.ts @@ -18,19 +18,39 @@ export function getStatusWithColor( const statusConfig = { done: { color: chalk.green, - icon: String.fromCharCode(8730), - tableIcon: String.fromCharCode(8730) - }, // √ - pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' }, + icon: 'āœ“', + tableIcon: 'āœ“' + }, + pending: { + color: chalk.yellow, + icon: 'ā—‹', + tableIcon: 'ā—‹' + }, 'in-progress': { color: chalk.hex('#FFA500'), - icon: String.fromCharCode(9654), - tableIcon: '>' - }, // ā–¶ - deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' }, - blocked: { color: chalk.red, icon: '!', tableIcon: '!' }, - review: { color: chalk.magenta, icon: '?', tableIcon: '?' }, - cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' } + icon: 'ā–¶', + tableIcon: 'ā–¶' + }, + deferred: { + color: chalk.gray, + icon: 'x', + tableIcon: 'x' + }, + review: { + color: chalk.magenta, + icon: '?', + tableIcon: '?' + }, + cancelled: { + color: chalk.gray, + icon: 'x', + tableIcon: 'x' + }, + blocked: { + color: chalk.red, + icon: '!', + tableIcon: '!' + } }; const config = statusConfig[status] || { @@ -39,18 +59,7 @@ export function getStatusWithColor( tableIcon: 'X' }; - // Use simple ASCII characters for stable display - const simpleIcons = { - done: String.fromCharCode(8730), // √ - pending: 'o', - 'in-progress': '>', - deferred: 'x', - blocked: '!', - review: '?', - cancelled: 'X' - }; - - const icon = forTable ? simpleIcons[status] || 'X' : config.icon; + const icon = forTable ? config.tableIcon : config.icon; return config.color(`${icon} ${status}`); } @@ -245,10 +254,24 @@ export function createTaskTable( } = options || {}; // Calculate dynamic column widths based on terminal width - const terminalWidth = process.stdout.columns || 100; + const terminalWidth = process.stdout.columns * 0.9 || 100; + // Adjust column widths to better match the original layout const baseColWidths = showComplexity - ? [8, Math.floor(terminalWidth * 0.35), 18, 12, 15, 12] // ID, Title, Status, Priority, Dependencies, Complexity - : [8, Math.floor(terminalWidth * 0.4), 18, 12, 20]; // ID, Title, Status, Priority, Dependencies + ? [ + Math.floor(terminalWidth * 0.06), + Math.floor(terminalWidth * 0.4), + Math.floor(terminalWidth * 0.15), + Math.floor(terminalWidth * 0.12), + Math.floor(terminalWidth * 0.2), + Math.floor(terminalWidth * 0.12) + ] // ID, Title, Status, Priority, Dependencies, Complexity + : [ + Math.floor(terminalWidth * 0.08), + Math.floor(terminalWidth * 0.4), + Math.floor(terminalWidth * 0.18), + Math.floor(terminalWidth * 0.12), + Math.floor(terminalWidth * 0.2) + ]; // ID, Title, Status, Priority, Dependencies const headers = [ chalk.blue.bold('ID'), @@ -284,11 +307,19 @@ export function createTaskTable( ]; if (showDependencies) { - row.push(formatDependenciesWithStatus(task.dependencies, tasks)); + // For table display, show simple format without status icons + if (!task.dependencies || task.dependencies.length === 0) { + row.push(chalk.gray('None')); + } else { + row.push( + chalk.cyan(task.dependencies.map((d) => String(d)).join(', ')) + ); + } } - if (showComplexity && 'complexity' in task) { - row.push(getComplexityWithColor(task.complexity as number | string)); + if (showComplexity) { + // Show N/A if no complexity score + row.push(chalk.gray('N/A')); } table.push(row); diff --git a/bin/task-master.js b/bin/task-master.js index a7d01c07..c3fc279f 100755 --- a/bin/task-master.js +++ b/bin/task-master.js @@ -20,357 +20,8 @@ * Main entry point for globally installed package */ -import { fileURLToPath } from 'url'; -import { dirname, resolve } from 'path'; -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'; +// Direct imports instead of spawning child processes +import { runCLI } from '../scripts/modules/commands.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -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 ', 'Project name') -// .option('-d, --description ', 'Project description') -// .option('-v, --version ', 'Project version') -// .option('-a, --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 }; +// Simply run the CLI directly +runCLI(); diff --git a/package-lock.json b/package-lock.json index 99913321..eefd1998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,8 @@ "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", + "marked": "^15.0.12", + "marked-terminal": "^7.3.0", "ollama-ai-provider": "^1.2.0", "openai": "^4.89.0", "ora": "^8.2.0", @@ -70,6 +72,7 @@ "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.28.1", "@types/jest": "^29.5.14", + "@types/marked-terminal": "^6.1.1", "concurrently": "^9.2.1", "cross-env": "^10.0.0", "dotenv-mono": "^1.5.1", @@ -10101,6 +10104,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -10261,6 +10271,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/marked-terminal": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/marked-terminal/-/marked-terminal-6.1.1.tgz", + "integrity": "sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cardinal": "^2.1", + "@types/node": "*", + "chalk": "^5.3.0", + "marked": ">=6.0.0 <12" + } + }, + "node_modules/@types/marked-terminal/node_modules/marked": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -12860,7 +12896,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -14895,6 +14930,12 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -15064,7 +15105,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -22303,7 +22343,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", - "optional": true, "bin": { "marked": "bin/marked.js" }, @@ -22311,6 +22350,54 @@ "node": ">= 18" } }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-escapes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", + "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked-terminal/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -24134,6 +24221,33 @@ "node": ">=10.5.0" } }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-emoji/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -27643,6 +27757,18 @@ "dev": true, "license": "MIT" }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -28446,6 +28572,34 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -29435,6 +29589,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unicorn-magic": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", diff --git a/package.json b/package.json index 597e0968..0029c5c2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "workspaces": ["apps/*", "packages/*", "."], "scripts": { "build": "npm run build:build-config && tsup", - "dev": "tsup --watch", + "dev": "tsup --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'", "turbo:dev": "turbo dev", "turbo:build": "turbo build", "turbo:typecheck": "turbo typecheck", @@ -59,7 +59,6 @@ "license": "MIT WITH Commons-Clause", "dependencies": { "@ai-sdk/amazon-bedrock": "^2.2.9", - "@tm/cli": "*", "@ai-sdk/anthropic": "^1.2.10", "@ai-sdk/azure": "^1.3.17", "@ai-sdk/google": "^1.2.13", @@ -74,6 +73,7 @@ "@inquirer/search": "^3.0.15", "@openrouter/ai-sdk-provider": "^0.4.5", "@streamparser/json": "^0.0.22", + "@tm/cli": "*", "ai": "^4.3.10", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", @@ -97,6 +97,8 @@ "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", + "marked": "^15.0.12", + "marked-terminal": "^7.3.0", "ollama-ai-provider": "^1.2.0", "openai": "^4.89.0", "ora": "^8.2.0", @@ -131,6 +133,7 @@ "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.28.1", "@types/jest": "^29.5.14", + "@types/marked-terminal": "^6.1.1", "concurrently": "^9.2.1", "cross-env": "^10.0.0", "dotenv-mono": "^1.5.1", diff --git a/packages/tm-core/package.json b/packages/tm-core/package.json index 85a62665..1bb0d868 100644 --- a/packages/tm-core/package.json +++ b/packages/tm-core/package.json @@ -7,54 +7,19 @@ "types": "./src/index.ts", "main": "./dist/index.js", "exports": { - ".": { - "types": "./src/index.ts", - "import": "./dist/index.js" - }, - "./auth": { - "types": "./src/auth/index.ts", - "import": "./dist/auth/index.js" - }, - "./storage": { - "types": "./src/storage/index.ts", - "import": "./dist/storage/index.js" - }, - "./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" - } + ".": "./src/index.ts", + "./auth": "./src/auth/index.ts", + "./storage": "./src/storage/index.ts", + "./config": "./src/config/index.ts", + "./providers": "./src/providers/index.ts", + "./services": "./src/services/index.ts", + "./errors": "./src/errors/index.ts", + "./logger": "./src/logger/index.ts", + "./types": "./src/types/index.ts", + "./interfaces": "./src/interfaces/index.ts", + "./utils": "./src/utils/index.ts" }, "scripts": { - "build": "tsup", - "dev": "tsup --watch", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/packages/tm-core/tsup.config.ts b/packages/tm-core/tsup.config.ts deleted file mode 100644 index e01e7a84..00000000 --- a/packages/tm-core/tsup.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineConfig } from 'tsup'; -import { baseConfig, mergeConfig } from '@tm/build-config'; -import { load as dotenvLoad } from 'dotenv-mono'; - -dotenvLoad(); - -// Get all TM_PUBLIC_* env variables for build-time injection -const getBuildTimeEnvs = () => { - const envs: Record = {}; - for (const [key, value] of Object.entries(process.env)) { - if (key.startsWith('TM_PUBLIC_')) { - // Return the actual value, not JSON.stringify'd - envs[key] = value || ''; - } - } - return envs; -}; - -export default defineConfig( - mergeConfig(baseConfig, { - entry: { - index: 'src/index.ts', - 'auth/index': 'src/auth/index.ts', - 'config/index': 'src/config/index.ts', - 'errors/index': 'src/errors/index.ts', - 'interfaces/index': 'src/interfaces/index.ts', - 'logger/index': 'src/logger/index.ts', - 'parser/index': 'src/parser/index.ts', - 'providers/index': 'src/providers/index.ts', - 'services/index': 'src/services/index.ts', - 'storage/index': 'src/storage/index.ts', - 'types/index': 'src/types/index.ts', - 'utils/index': 'src/utils/index.ts' - }, - format: ['esm'], - dts: true, - outDir: 'dist', - // Replace process.env.TM_PUBLIC_* with actual values at build time - env: getBuildTimeEnvs() - }) -); diff --git a/src/utils/getVersion.js b/src/utils/getVersion.js index 55a64f40..ffbd1f5e 100644 --- a/src/utils/getVersion.js +++ b/src/utils/getVersion.js @@ -1,7 +1,4 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { log } from '../../scripts/modules/utils.js'; +import packageJson from '../../package.json' with { type: 'json' }; /** * Reads the version from the nearest package.json relative to this file. @@ -9,27 +6,5 @@ import { log } from '../../scripts/modules/utils.js'; * @returns {string} The version string or 'unknown'. */ export function getTaskMasterVersion() { - let version = 'unknown'; - try { - // Get the directory of the current module (getPackageVersion.js) - const currentModuleFilename = fileURLToPath(import.meta.url); - const currentModuleDirname = path.dirname(currentModuleFilename); - // Construct the path to package.json relative to this file (../../package.json) - const packageJsonPath = path.join( - currentModuleDirname, - '..', - '..', - 'package.json' - ); - - if (fs.existsSync(packageJsonPath)) { - const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); - const packageJson = JSON.parse(packageJsonContent); - version = packageJson.version; - } - } catch (error) { - // Silently fall back to default version - log('warn', 'Could not read own package.json for version info.', error); - } - return version; + return packageJson.version || 'unknown'; } diff --git a/tsconfig.json b/tsconfig.json index d6057af0..58a2d25b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,15 @@ "strict": true, "skipLibCheck": true, "noEmit": true, - "baseUrl": "." + "baseUrl": ".", + "paths": { + "@tm/core": ["./packages/tm-core/src/index.ts"], + "@tm/core/*": ["./packages/tm-core/src/*"], + "@tm/cli": ["./apps/cli/src/index.ts"], + "@tm/cli/*": ["./apps/cli/src/*"], + "@tm/build-config": ["./packages/build-config/src/index.ts"], + "@tm/build-config/*": ["./packages/build-config/src/*"] + } }, "tsx": { "tsconfig": { diff --git a/tsup.config.ts b/tsup.config.ts index 3f301d71..eb007fc6 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,26 +1,97 @@ import { defineConfig } from 'tsup'; import { baseConfig, mergeConfig } from '@tm/build-config'; +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(); + +// Get all TM_PUBLIC_* env variables for build-time injection +const getBuildTimeEnvs = () => { + const envs: Record = {}; + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('TM_PUBLIC_')) { + // Return the actual value, not JSON.stringify'd + envs[key] = value || ''; + } + } + return envs; +}; export default defineConfig( mergeConfig(baseConfig, { entry: { - 'task-master': 'bin/task-master.js', + 'task-master': 'scripts/dev.js', 'mcp-server': 'mcp-server/server.js' }, outDir: 'dist', publicDir: 'public', - // Bundle our monorepo packages but keep node_modules external - noExternal: [/@tm\/.*/], - // Ensure no code splitting - splitting: false, - // Better watch configuration - ignoreWatch: [ - 'dist', - 'node_modules', - '.git', - 'tests', - '*.test.*', - '*.spec.*' - ] + // Override the base config's external to bundle our workspace packages + noExternal: [/^@tm\//], + external: [ + /^@supabase\//, // Keep Supabase external to avoid dynamic require issues + 'marked', + 'marked-terminal' + ], + env: getBuildTimeEnvs(), + esbuildOptions(options) { + // Set up path aliases for workspace packages + options.alias = { + '@tm/core': path.resolve(__dirname, 'packages/tm-core/src/index.ts'), + '@tm/core/auth': path.resolve( + __dirname, + 'packages/tm-core/src/auth/index.ts' + ), + '@tm/core/storage': path.resolve( + __dirname, + 'packages/tm-core/src/storage/index.ts' + ), + '@tm/core/config': path.resolve( + __dirname, + 'packages/tm-core/src/config/index.ts' + ), + '@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' + ), + '@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' + ) + }; + } }) ); diff --git a/turbo.json b/turbo.json index 4ac183fd..50523359 100644 --- a/turbo.json +++ b/turbo.json @@ -10,7 +10,6 @@ "dev": { "cache": false, "persistent": true, - "dependsOn": ["^build"], "inputs": [ "$TURBO_DEFAULT$", "!{packages,apps}/**/dist/**",