diff --git a/apps/cli/src/commands/set-status.command.ts b/apps/cli/src/commands/set-status.command.ts index dbf35329..d6ae39c7 100644 --- a/apps/cli/src/commands/set-status.command.ts +++ b/apps/cli/src/commands/set-status.command.ts @@ -288,6 +288,26 @@ export class SetStatusCommand extends Command { getLastResult(): SetStatusResult | undefined { return this.lastResult; } + + /** + * Static method to register this command on an existing program + * This is for gradual migration - allows commands.js to use this + */ + static registerOn(program: Command): Command { + const setStatusCommand = new SetStatusCommand(); + program.addCommand(setStatusCommand); + return setStatusCommand; + } + + /** + * Alternative registration that returns the command for chaining + * Can also configure the command name if needed + */ + static register(program: Command, name?: string): SetStatusCommand { + const setStatusCommand = new SetStatusCommand(name); + program.addCommand(setStatusCommand); + return setStatusCommand; + } } /** diff --git a/apps/extension/package.json b/apps/extension/package.json index 9975072a..6a25c722 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -3,7 +3,7 @@ "private": true, "displayName": "TaskMaster", "description": "A visual Kanban board interface for TaskMaster projects in VS Code", - "version": "0.25.0-rc.0", + "version": "0.24.2", "publisher": "Hamster", "icon": "assets/icon.png", "engines": { diff --git a/package-lock.json b/package-lock.json index 362d06c6..c1f90a25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -365,7 +365,7 @@ } }, "apps/extension": { - "version": "0.25.0-rc.0", + "version": "0.24.2", "dependencies": { "task-master-ai": "*" }, diff --git a/package.json b/package.json index 4ce189f4..d7bd55e5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "workspaces": ["apps/*", "packages/*", "."], "scripts": { "build": "npm run build:build-config && cross-env NODE_ENV=production tsdown", - "dev": "tsdown --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'", + "dev": "tsdown --watch", "turbo:dev": "turbo dev", "turbo:build": "turbo build", "turbo:typecheck": "turbo typecheck", diff --git a/packages/tm-core/src/interfaces/storage.interface.ts b/packages/tm-core/src/interfaces/storage.interface.ts index d10427d8..4fee9893 100644 --- a/packages/tm-core/src/interfaces/storage.interface.ts +++ b/packages/tm-core/src/interfaces/storage.interface.ts @@ -17,6 +17,14 @@ export interface IStorage { */ loadTasks(tag?: string): Promise; + /** + * Load a single task by ID + * @param taskId - ID of the task to load + * @param tag - Optional tag context for the task + * @returns Promise that resolves to the task or null if not found + */ + loadTask(taskId: string, tag?: string): Promise; + /** * Save tasks to storage, replacing existing tasks * @param tasks - Array of tasks to save @@ -175,6 +183,7 @@ export abstract class BaseStorage implements IStorage { // Abstract methods that must be implemented by concrete classes abstract loadTasks(tag?: string): Promise; + abstract loadTask(taskId: string, tag?: string): Promise; abstract saveTasks(tasks: Task[], tag?: string): Promise; abstract appendTasks(tasks: Task[], tag?: string): Promise; abstract updateTask( diff --git a/packages/tm-core/src/mappers/TaskMapper.ts b/packages/tm-core/src/mappers/TaskMapper.ts index f1b9be2b..89f5a8eb 100644 --- a/packages/tm-core/src/mappers/TaskMapper.ts +++ b/packages/tm-core/src/mappers/TaskMapper.ts @@ -127,7 +127,7 @@ export class TaskMapper { /** * Maps database status to internal status */ - private static mapStatus( + static mapStatus( status: Database['public']['Enums']['task_status'] ): Task['status'] { switch (status) { diff --git a/packages/tm-core/src/repositories/supabase-task-repository.ts b/packages/tm-core/src/repositories/supabase-task-repository.ts index c448722b..48689496 100644 --- a/packages/tm-core/src/repositories/supabase-task-repository.ts +++ b/packages/tm-core/src/repositories/supabase-task-repository.ts @@ -3,6 +3,30 @@ import { Task } from '../types/index.js'; import { Database } from '../types/database.types.js'; import { TaskMapper } from '../mappers/TaskMapper.js'; import { AuthManager } from '../auth/auth-manager.js'; +import { z } from 'zod'; + +// Zod schema for task status validation +const TaskStatusSchema = z.enum([ + 'pending', + 'in-progress', + 'done', + 'review', + 'deferred', + 'cancelled', + 'blocked' +]); + +// Zod schema for task updates +const TaskUpdateSchema = z + .object({ + title: z.string().min(1).optional(), + description: z.string().optional(), + status: TaskStatusSchema.optional(), + priority: z.enum(['low', 'medium', 'high', 'critical']).optional(), + details: z.string().optional(), + testStrategy: z.string().optional() + }) + .partial(); export class SupabaseTaskRepository { constructor(private supabase: SupabaseClient) {} @@ -60,12 +84,22 @@ export class SupabaseTaskRepository { return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []); } - async getTask(accountId: string, taskId: string): Promise { + async getTask(_projectId: string, taskId: string): Promise { + // Get the current context to determine briefId (projectId not used in Supabase context) + const authManager = AuthManager.getInstance(); + const context = authManager.getContext(); + + if (!context || !context.briefId) { + throw new Error( + 'No brief selected. Please select a brief first using: tm context brief' + ); + } + const { data, error } = await this.supabase .from('tasks') .select('*') - .eq('account_id', accountId) - .ilike('display_id', taskId) + .eq('brief_id', context.briefId) + .eq('display_id', taskId.toUpperCase()) .single(); if (error) { @@ -107,4 +141,85 @@ export class SupabaseTaskRepository { dependenciesByTaskId ); } + + async updateTask( + projectId: string, + taskId: string, + updates: Partial + ): Promise { + + // Get the current context to determine briefId + const authManager = AuthManager.getInstance(); + const context = authManager.getContext(); + + if (!context || !context.briefId) { + throw new Error( + 'No brief selected. Please select a brief first using: tm context brief' + ); + } + + // Validate updates using Zod schema + try { + TaskUpdateSchema.parse(updates); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join('.')}: ${err.message}`) + .join(', '); + throw new Error(`Invalid task update data: ${errorMessages}`); + } + throw error; + } + + // Convert Task fields to database fields - only include fields that actually exist in the database + const dbUpdates: any = {}; + + if (updates.title !== undefined) dbUpdates.title = updates.title; + if (updates.description !== undefined) + dbUpdates.description = updates.description; + if (updates.status !== undefined) + dbUpdates.status = this.mapStatusToDatabase(updates.status); + if (updates.priority !== undefined) dbUpdates.priority = updates.priority; + // Skip fields that don't exist in database schema: details, testStrategy, etc. + + // Update the task + const { error } = await this.supabase + .from('tasks') + .update(dbUpdates) + .eq('brief_id', context.briefId) + .eq('display_id', taskId.toUpperCase()); + + if (error) { + throw new Error(`Failed to update task: ${error.message}`); + } + + // Return the updated task by fetching it + const updatedTask = await this.getTask(projectId, taskId); + if (!updatedTask) { + throw new Error(`Failed to retrieve updated task ${taskId}`); + } + + return updatedTask; + } + + /** + * Maps internal status to database status + */ + private mapStatusToDatabase( + status: string + ): Database['public']['Enums']['task_status'] { + switch (status) { + case 'pending': + return 'todo'; + case 'in-progress': + case 'in_progress': // Accept both formats + return 'in_progress'; + case 'done': + return 'done'; + default: + throw new Error( + `Invalid task status: ${status}. Valid statuses are: pending, in-progress, done` + ); + } + } } diff --git a/packages/tm-core/src/services/task-service.ts b/packages/tm-core/src/services/task-service.ts index 489d6c48..38160b64 100644 --- a/packages/tm-core/src/services/task-service.ts +++ b/packages/tm-core/src/services/task-service.ts @@ -374,6 +374,7 @@ export class TaskService { newStatus: TaskStatus; taskId: string; }> { + // Ensure we have storage if (!this.storage) { throw new TaskMasterError( @@ -385,73 +386,43 @@ export class TaskService { // Use provided tag or get active tag const activeTag = tag || this.getActiveTag(); - // Get all tasks to find the one to update - const result = await this.getTaskList({ - tag: activeTag, - includeSubtasks: true - }); - - // Handle both regular tasks (e.g., "5") and subtasks (e.g., "5.2") const taskIdStr = String(taskId); - let taskToUpdate: Task | undefined; - let oldStatus: TaskStatus; + // TODO: For now, assume it's a regular task and just try to update directly + // In the future, we can add subtask support if needed if (taskIdStr.includes('.')) { - // Handle subtask - const [parentIdStr, subtaskIdStr] = taskIdStr.split('.'); - const parentId = parseInt(parentIdStr, 10); - const subtaskId = parseInt(subtaskIdStr, 10); - - const parentTask = result.tasks.find((t) => t.id === String(parentId)); - if (!parentTask || !parentTask.subtasks) { - throw new TaskMasterError( - `Parent task ${parentId} not found or has no subtasks`, - ERROR_CODES.TASK_NOT_FOUND - ); - } - - const subtask = parentTask.subtasks.find( - (st) => String(st.id) === String(subtaskId) + throw new TaskMasterError( + 'Subtask status updates not yet supported in API storage', + ERROR_CODES.NOT_IMPLEMENTED ); - if (!subtask) { - throw new TaskMasterError( - `Subtask ${taskIdStr} not found`, - ERROR_CODES.TASK_NOT_FOUND - ); - } - - oldStatus = subtask.status; - - // Update the subtask status - subtask.status = newStatus; - - // Update the parent task with the modified subtask - await this.storage.updateTask(parentTask.id, parentTask, activeTag); - - taskToUpdate = parentTask; - } else { - // Handle regular task - const taskIdNum = parseInt(taskIdStr, 10); - taskToUpdate = result.tasks.find( - (t) => String(t.id) === String(taskIdNum) - ); - - if (!taskToUpdate) { - throw new TaskMasterError( - `Task ${taskIdStr} not found`, - ERROR_CODES.TASK_NOT_FOUND - ); - } - - oldStatus = taskToUpdate.status; - - // Update the task status - taskToUpdate.status = newStatus; - - // Save the updated task - await this.storage.updateTask(taskToUpdate.id, taskToUpdate, activeTag); } + // Get the current task to get old status (simple, direct approach) + let currentTask: Task | null; + try { + // Try to get the task directly + currentTask = await this.storage.loadTask(taskIdStr, activeTag); + } catch (error) { + throw new TaskMasterError( + `Failed to load task ${taskIdStr}`, + ERROR_CODES.TASK_NOT_FOUND, + { taskId: taskIdStr }, + error as Error + ); + } + + if (!currentTask) { + throw new TaskMasterError( + `Task ${taskIdStr} not found`, + ERROR_CODES.TASK_NOT_FOUND + ); + } + + const oldStatus = currentTask.status; + + // Simple, direct update - just change the status + await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag); + return { success: true, oldStatus, diff --git a/packages/tm-core/src/storage/api-storage.ts b/packages/tm-core/src/storage/api-storage.ts index e23eb0ca..a6f74ba8 100644 --- a/packages/tm-core/src/storage/api-storage.ts +++ b/packages/tm-core/src/storage/api-storage.ts @@ -223,14 +223,6 @@ export class ApiStorage implements IStorage { await this.ensureInitialized(); try { - if (tag) { - // Check if task is in tag - const tagData = this.tagsCache.get(tag); - if (!tagData || !tagData.tasks.includes(taskId)) { - return null; - } - } - return await this.retryOperation(() => this.repository.getTask(this.projectId, taskId) ); @@ -477,6 +469,7 @@ export class ApiStorage implements IStorage { updates: Partial, tag?: string ): Promise { + await this.ensureInitialized(); try { diff --git a/packages/tm-core/src/storage/file-storage/file-storage.ts b/packages/tm-core/src/storage/file-storage/file-storage.ts index bcef5488..66074a43 100644 --- a/packages/tm-core/src/storage/file-storage/file-storage.ts +++ b/packages/tm-core/src/storage/file-storage/file-storage.ts @@ -102,6 +102,14 @@ export class FileStorage implements IStorage { } } + /** + * Load a single task by ID from the tasks.json file + */ + async loadTask(taskId: string, tag?: string): Promise { + const tasks = await this.loadTasks(tag); + return tasks.find(task => task.id === taskId) || null; + } + /** * Save tasks for a specific tag in the single tasks.json file */ diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 325c9c72..1024b0e2 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -20,14 +20,14 @@ import { ListTasksCommand, ShowCommand, AuthCommand, - ContextCommand + ContextCommand, + SetStatusCommand } from '@tm/cli'; import { parsePRD, updateTasks, generateTaskFiles, - setTaskStatus, listTasks, expandTask, expandAllTasks, @@ -1684,63 +1684,9 @@ function registerCommands(programInstance) { }); }); - // set-status command - programInstance - .command('set-status') - .alias('mark') - .alias('set') - .description('Set the status of a task') - .option( - '-i, --id ', - 'Task ID (can be comma-separated for multiple tasks)' - ) - .option( - '-s, --status ', - `New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})` - ) - .option( - '-f, --file ', - 'Path to the tasks file', - TASKMASTER_TASKS_FILE - ) - .option('--tag ', 'Specify tag context for task operations') - .action(async (options) => { - // Initialize TaskMaster - const taskMaster = initTaskMaster({ - tasksPath: options.file || true, - tag: options.tag - }); - - const taskId = options.id; - const status = options.status; - - if (!taskId || !status) { - console.error(chalk.red('Error: Both --id and --status are required')); - process.exit(1); - } - - if (!isValidTaskStatus(status)) { - console.error( - chalk.red( - `Error: Invalid status value: ${status}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` - ) - ); - - process.exit(1); - } - const tag = taskMaster.getCurrentTag(); - - displayCurrentTagIndicator(tag); - - console.log( - chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`) - ); - - await setTaskStatus(taskMaster.getTasksPath(), taskId, status, { - projectRoot: taskMaster.getProjectRoot(), - tag - }); - }); + // Register the set-status command from @tm/cli + // Handles task status updates with proper error handling and validation + SetStatusCommand.registerOn(programInstance); // NEW: Register the new list command from @tm/cli // This command handles all its own configuration and logic