feat: finalize set-status
This commit is contained in:
@@ -288,6 +288,26 @@ export class SetStatusCommand extends Command {
|
|||||||
getLastResult(): SetStatusResult | undefined {
|
getLastResult(): SetStatusResult | undefined {
|
||||||
return this.lastResult;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.0-rc.0",
|
"version": "0.24.2",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -365,7 +365,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.0-rc.0",
|
"version": "0.24.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "*"
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"workspaces": ["apps/*", "packages/*", "."],
|
"workspaces": ["apps/*", "packages/*", "."],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
|
"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:dev": "turbo dev",
|
||||||
"turbo:build": "turbo build",
|
"turbo:build": "turbo build",
|
||||||
"turbo:typecheck": "turbo typecheck",
|
"turbo:typecheck": "turbo typecheck",
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export interface IStorage {
|
|||||||
*/
|
*/
|
||||||
loadTasks(tag?: string): Promise<Task[]>;
|
loadTasks(tag?: string): Promise<Task[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Task | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save tasks to storage, replacing existing tasks
|
* Save tasks to storage, replacing existing tasks
|
||||||
* @param tasks - Array of tasks to save
|
* @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 methods that must be implemented by concrete classes
|
||||||
abstract loadTasks(tag?: string): Promise<Task[]>;
|
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||||
|
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract updateTask(
|
abstract updateTask(
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class TaskMapper {
|
|||||||
/**
|
/**
|
||||||
* Maps database status to internal status
|
* Maps database status to internal status
|
||||||
*/
|
*/
|
||||||
private static mapStatus(
|
static mapStatus(
|
||||||
status: Database['public']['Enums']['task_status']
|
status: Database['public']['Enums']['task_status']
|
||||||
): Task['status'] {
|
): Task['status'] {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -3,6 +3,30 @@ import { Task } from '../types/index.js';
|
|||||||
import { Database } from '../types/database.types.js';
|
import { Database } from '../types/database.types.js';
|
||||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||||
import { AuthManager } from '../auth/auth-manager.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 {
|
export class SupabaseTaskRepository {
|
||||||
constructor(private supabase: SupabaseClient<Database>) {}
|
constructor(private supabase: SupabaseClient<Database>) {}
|
||||||
@@ -60,12 +84,22 @@ export class SupabaseTaskRepository {
|
|||||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTask(accountId: string, taskId: string): Promise<Task | null> {
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
|
// 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
|
const { data, error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('account_id', accountId)
|
.eq('brief_id', context.briefId)
|
||||||
.ilike('display_id', taskId)
|
.eq('display_id', taskId.toUpperCase())
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -107,4 +141,85 @@ export class SupabaseTaskRepository {
|
|||||||
dependenciesByTaskId
|
dependenciesByTaskId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateTask(
|
||||||
|
projectId: string,
|
||||||
|
taskId: string,
|
||||||
|
updates: Partial<Task>
|
||||||
|
): Promise<Task> {
|
||||||
|
|
||||||
|
// 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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ export class TaskService {
|
|||||||
newStatus: TaskStatus;
|
newStatus: TaskStatus;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
}> {
|
}> {
|
||||||
|
|
||||||
// Ensure we have storage
|
// Ensure we have storage
|
||||||
if (!this.storage) {
|
if (!this.storage) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
@@ -385,72 +386,42 @@ export class TaskService {
|
|||||||
// Use provided tag or get active tag
|
// Use provided tag or get active tag
|
||||||
const activeTag = tag || this.getActiveTag();
|
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);
|
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('.')) {
|
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(
|
throw new TaskMasterError(
|
||||||
`Parent task ${parentId} not found or has no subtasks`,
|
'Subtask status updates not yet supported in API storage',
|
||||||
ERROR_CODES.TASK_NOT_FOUND
|
ERROR_CODES.NOT_IMPLEMENTED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtask = parentTask.subtasks.find(
|
// Get the current task to get old status (simple, direct approach)
|
||||||
(st) => String(st.id) === String(subtaskId)
|
let currentTask: Task | null;
|
||||||
);
|
try {
|
||||||
if (!subtask) {
|
// Try to get the task directly
|
||||||
|
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
|
||||||
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
`Subtask ${taskIdStr} not found`,
|
`Failed to load task ${taskIdStr}`,
|
||||||
ERROR_CODES.TASK_NOT_FOUND
|
ERROR_CODES.TASK_NOT_FOUND,
|
||||||
|
{ taskId: taskIdStr },
|
||||||
|
error as Error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oldStatus = subtask.status;
|
if (!currentTask) {
|
||||||
|
|
||||||
// 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(
|
throw new TaskMasterError(
|
||||||
`Task ${taskIdStr} not found`,
|
`Task ${taskIdStr} not found`,
|
||||||
ERROR_CODES.TASK_NOT_FOUND
|
ERROR_CODES.TASK_NOT_FOUND
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oldStatus = taskToUpdate.status;
|
const oldStatus = currentTask.status;
|
||||||
|
|
||||||
// Update the task status
|
// Simple, direct update - just change the status
|
||||||
taskToUpdate.status = newStatus;
|
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
|
||||||
|
|
||||||
// Save the updated task
|
|
||||||
await this.storage.updateTask(taskToUpdate.id, taskToUpdate, activeTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -223,14 +223,6 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
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(() =>
|
return await this.retryOperation(() =>
|
||||||
this.repository.getTask(this.projectId, taskId)
|
this.repository.getTask(this.projectId, taskId)
|
||||||
);
|
);
|
||||||
@@ -477,6 +469,7 @@ export class ApiStorage implements IStorage {
|
|||||||
updates: Partial<Task>,
|
updates: Partial<Task>,
|
||||||
tag?: string
|
tag?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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<Task | null> {
|
||||||
|
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
|
* Save tasks for a specific tag in the single tasks.json file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ import {
|
|||||||
ListTasksCommand,
|
ListTasksCommand,
|
||||||
ShowCommand,
|
ShowCommand,
|
||||||
AuthCommand,
|
AuthCommand,
|
||||||
ContextCommand
|
ContextCommand,
|
||||||
|
SetStatusCommand
|
||||||
} from '@tm/cli';
|
} from '@tm/cli';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parsePRD,
|
parsePRD,
|
||||||
updateTasks,
|
updateTasks,
|
||||||
generateTaskFiles,
|
generateTaskFiles,
|
||||||
setTaskStatus,
|
|
||||||
listTasks,
|
listTasks,
|
||||||
expandTask,
|
expandTask,
|
||||||
expandAllTasks,
|
expandAllTasks,
|
||||||
@@ -1684,63 +1684,9 @@ function registerCommands(programInstance) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// set-status command
|
// Register the set-status command from @tm/cli
|
||||||
programInstance
|
// Handles task status updates with proper error handling and validation
|
||||||
.command('set-status')
|
SetStatusCommand.registerOn(programInstance);
|
||||||
.alias('mark')
|
|
||||||
.alias('set')
|
|
||||||
.description('Set the status of a task')
|
|
||||||
.option(
|
|
||||||
'-i, --id <id>',
|
|
||||||
'Task ID (can be comma-separated for multiple tasks)'
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
'-s, --status <status>',
|
|
||||||
`New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})`
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
'-f, --file <file>',
|
|
||||||
'Path to the tasks file',
|
|
||||||
TASKMASTER_TASKS_FILE
|
|
||||||
)
|
|
||||||
.option('--tag <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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// NEW: Register the new list command from @tm/cli
|
// NEW: Register the new list command from @tm/cli
|
||||||
// This command handles all its own configuration and logic
|
// This command handles all its own configuration and logic
|
||||||
|
|||||||
Reference in New Issue
Block a user