From cd2b4c9d56f78e2d3021e2cfc09a97d9a8e6f1b8 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:14:13 -0700 Subject: [PATCH] feat: improve table UI --- apps/cli/package.json | 12 +- apps/cli/src/commands/list.command.ts | 129 ++++++- .../src/ui/components/dashboard.component.ts | 290 +++++++++++--- .../cli/src/ui/components/header.component.ts | 17 +- apps/cli/tsconfig.json | 54 +-- bin/task-master.js | 357 +----------------- packages/tm-core/package.json | 55 +-- tsup.config.ts | 46 ++- 8 files changed, 448 insertions(+), 512 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 6252fc20..82d47dda 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,10 +6,7 @@ "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": { @@ -46,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 3520efb0..b9b97dfd 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -280,16 +280,8 @@ export class ListTasksCommand extends Command { const depStats = calculateDependencyStatistics(tasks); const priorityBreakdown = getPriorityBreakdown(tasks); - // Find next task (simplified for now) - const nextTask: NextTaskInfo | undefined = 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]; + // Find next task following the same logic as findNextTask + const nextTask = this.findNextTask(tasks); // Display dashboard boxes displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask); @@ -311,6 +303,123 @@ 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/ui/components/dashboard.component.ts b/apps/cli/src/ui/components/dashboard.component.ts index cc9c741f..56ffeb70 100644 --- a/apps/cli/src/ui/components/dashboard.component.ts +++ b/apps/cli/src/ui/components/dashboard.component.ts @@ -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 { - const filled = Math.round((percentage / 100) * width); - const empty = width - filled; - - const bar = chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty); +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; } @@ -72,7 +172,7 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics { completionPercentage: 0 }; - tasks.forEach(task => { + tasks.forEach((task) => { switch (task.status) { case 'done': stats.done++; @@ -98,9 +198,8 @@ export function calculateTaskStatistics(tasks: Task[]): TaskStatistics { } }); - stats.completionPercentage = stats.total > 0 - ? Math.round((stats.done / stats.total) * 100) - : 0; + stats.completionPercentage = + stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; return stats; } @@ -121,9 +220,9 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { completionPercentage: 0 }; - tasks.forEach(task => { + tasks.forEach((task) => { if (task.subtasks && task.subtasks.length > 0) { - task.subtasks.forEach(subtask => { + task.subtasks.forEach((subtask) => { stats.total++; switch (subtask.status) { case 'done': @@ -152,9 +251,8 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { } }); - stats.completionPercentage = stats.total > 0 - ? Math.round((stats.done / stats.total) * 100) - : 0; + stats.completionPercentage = + stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0; return stats; } @@ -162,34 +260,39 @@ export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { /** * Calculate dependency statistics */ -export function calculateDependencyStatistics(tasks: Task[]): DependencyStatistics { +export function calculateDependencyStatistics( + tasks: Task[] +): DependencyStatistics { 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( - t => t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) + (t) => + t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) ).length; const tasksWithAllDepsSatisfied = tasks.filter( - t => t.status !== 'done' && + (t) => + t.status !== 'done' && t.dependencies && t.dependencies.length > 0 && - t.dependencies.every(depId => completedTaskIds.has(depId)) + t.dependencies.every((depId) => completedTaskIds.has(depId)) ).length; const tasksBlockedByDeps = tasks.filter( - t => t.status !== 'done' && + (t) => + t.status !== 'done' && t.dependencies && t.dependencies.length > 0 && - !t.dependencies.every(depId => completedTaskIds.has(depId)) + !t.dependencies.every((depId) => completedTaskIds.has(depId)) ).length; // Calculate most depended-on task const dependencyCount: Record = {}; - tasks.forEach(task => { + tasks.forEach((task) => { if (task.dependencies && task.dependencies.length > 0) { - task.dependencies.forEach(depId => { + task.dependencies.forEach((depId) => { const key = String(depId); dependencyCount[key] = (dependencyCount[key] || 0) + 1; }); @@ -198,7 +301,7 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti let mostDependedOnTaskId: number | undefined; let mostDependedOnCount = 0; - + for (const [taskId, count] of Object.entries(dependencyCount)) { if (count > mostDependedOnCount) { mostDependedOnCount = count; @@ -211,9 +314,8 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), 0 ); - const avgDependenciesPerTask = tasks.length > 0 - ? totalDependencies / tasks.length - : 0; + const avgDependenciesPerTask = + tasks.length > 0 ? totalDependencies / tasks.length : 0; return { tasksWithNoDeps, @@ -228,7 +330,9 @@ export function calculateDependencyStatistics(tasks: Task[]): DependencyStatisti /** * Get priority counts */ -export function getPriorityBreakdown(tasks: Task[]): Record { +export function getPriorityBreakdown( + tasks: Task[] +): Record { const breakdown: Record = { critical: 0, high: 0, @@ -236,7 +340,7 @@ export function getPriorityBreakdown(tasks: Task[]): Record { + tasks.forEach((task) => { const priority = task.priority || 'medium'; breakdown[priority]++; }); @@ -244,6 +348,57 @@ export function getPriorityBreakdown(tasks: Task[]): Record ): string { - const taskProgressBar = createProgressBar(taskStats.completionPercentage); - const subtaskProgressBar = createProgressBar(subtaskStats.completionPercentage); + // Calculate status breakdowns using the helper function + const taskStatusBreakdown = calculateStatusBreakdown(taskStats); + const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats); - const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}%`; - const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}%`; + // Create progress bars with the breakdowns + const taskProgressBar = createProgressBar( + taskStats.completionPercentage, + 30, + taskStatusBreakdown + ); + const subtaskProgressBar = createProgressBar( + subtaskStats.completionPercentage, + 30, + subtaskStatusBreakdown + ); - const content = - chalk.white.bold('Project Dashboard') + '\n' + + 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` + - `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` + - `Cancelled: ${chalk.gray(taskStats.cancelled)}\n\n` + + formatStatusLine(taskStats, false) + + '\n\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` + - `Deferred: ${chalk.gray(subtaskStats.deferred)} Cancelled: ${chalk.gray(subtaskStats.cancelled)}\n\n` + - chalk.cyan.bold('Priority Breakdown:') + '\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}`; @@ -281,24 +451,33 @@ export function displayDependencyDashboard( depStats: DependencyStatistics, nextTask?: NextTaskInfo ): string { - const content = - chalk.white.bold('Dependency Status & Next Task') + '\n' + - chalk.cyan.bold('Dependency Metrics:') + '\n' + + 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)`) + 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' + + 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') + 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') + nextTask?.dependencies?.length + ? chalk.cyan(nextTask.dependencies.join(', ')) + : chalk.gray('None') }\n` + `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; @@ -315,8 +494,15 @@ export function displayDashboards( depStats: DependencyStatistics, nextTask?: NextTaskInfo ): void { - const projectDashboardContent = displayProjectDashboard(taskStats, subtaskStats, priorityBreakdown); - const dependencyDashboardContent = displayDependencyDashboard(depStats, nextTask); + const projectDashboardContent = displayProjectDashboard( + taskStats, + subtaskStats, + priorityBreakdown + ); + const dependencyDashboardContent = displayDependencyDashboard( + depStats, + nextTask + ); // Get terminal width const terminalWidth = process.stdout.columns || 80; @@ -378,4 +564,4 @@ export function displayDashboards( console.log(dashboardBox); console.log(dependencyBox); } -} \ No newline at end of file +} diff --git a/apps/cli/src/ui/components/header.component.ts b/apps/cli/src/ui/components/header.component.ts index 6423cb89..cc5c8598 100644 --- a/apps/cli/src/ui/components/header.component.ts +++ b/apps/cli/src/ui/components/header.component.ts @@ -7,6 +7,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; import figlet from 'figlet'; import gradient from 'gradient-string'; +import packageJson from '../../../package.json'; /** * Header configuration options @@ -40,7 +41,7 @@ function createBanner(): string { */ export function displayHeader(options: HeaderOptions = {}): void { const { - version = '0.26.0', + version = packageJson.version, projectName = 'Taskmaster', tag, filePath, @@ -50,7 +51,7 @@ export function displayHeader(options: HeaderOptions = {}): void { // Display the ASCII banner if requested if (showBanner) { console.log(createBanner()); - + // Add creator credit line below the banner console.log( 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 if (tag || filePath) { let tagInfo = ''; - + if (tag && tag !== 'master') { tagInfo = `🏷 tag: ${chalk.cyan(tag)}`; } else { @@ -84,9 +85,11 @@ export function displayHeader(options: HeaderOptions = {}): void { console.log(tagInfo); if (filePath) { - console.log( - `Listing tasks from: ${chalk.dim(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 @@ -98,4 +101,4 @@ export function displayHeader(options: HeaderOptions = {}): void { */ export function displaySimpleHeader(options: HeaderOptions = {}): void { displayHeader({ ...options, showBanner: false }); -} \ No newline at end of file +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index ec5cee3e..7cea8bd4 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -1,27 +1,31 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "lib": ["ES2022"], - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "resolveJsonModule": true, - "allowJs": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "types": ["node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "allowJs": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"], + "paths": { + "@tm/core": ["../../packages/tm-core/src/index.ts"], + "@tm/core/*": ["../../packages/tm-core/src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] } diff --git a/bin/task-master.js b/bin/task-master.js index a7d01c07..634dc145 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(); \ No newline at end of file diff --git a/packages/tm-core/package.json b/packages/tm-core/package.json index e16a83de..1bb0d868 100644 --- a/packages/tm-core/package.json +++ b/packages/tm-core/package.json @@ -7,50 +7,17 @@ "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": { "test": "vitest run", diff --git a/tsup.config.ts b/tsup.config.ts index 41f9dd14..efb9ca48 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,6 +1,10 @@ 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(); @@ -16,28 +20,38 @@ const getBuildTimeEnvs = () => { 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.*' - ], - env: getBuildTimeEnvs() + // Override the base config's external to bundle our workspace packages + noExternal: [/^@tm\//], + external: [/^@supabase\//], // Keep Supabase external to avoid dynamic require issues + 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'), + }; + } }) );