Compare commits
3 Commits
extension@
...
ralph/feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d0db63c93 | ||
|
|
d66f0e5789 | ||
|
|
3657e798e8 |
318
apps/cli/src/commands/set-status.command.ts
Normal file
318
apps/cli/src/commands/set-status.command.ts
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview SetStatusCommand using Commander's native class pattern
|
||||||
|
* Extends Commander.Command for better integration with the framework
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import boxen from 'boxen';
|
||||||
|
import {
|
||||||
|
createTaskMasterCore,
|
||||||
|
type TaskMasterCore,
|
||||||
|
type TaskStatus
|
||||||
|
} from '@tm/core';
|
||||||
|
import type { StorageType } from '@tm/core/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid task status values for validation
|
||||||
|
*/
|
||||||
|
const VALID_TASK_STATUSES: TaskStatus[] = [
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked',
|
||||||
|
'review'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options interface for the set-status command
|
||||||
|
*/
|
||||||
|
export interface SetStatusCommandOptions {
|
||||||
|
id?: string;
|
||||||
|
status?: TaskStatus;
|
||||||
|
format?: 'text' | 'json';
|
||||||
|
silent?: boolean;
|
||||||
|
project?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result type from set-status command
|
||||||
|
*/
|
||||||
|
export interface SetStatusResult {
|
||||||
|
success: boolean;
|
||||||
|
updatedTasks: Array<{
|
||||||
|
taskId: string;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
}>;
|
||||||
|
storageType: Exclude<StorageType, 'auto'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetStatusCommand extending Commander's Command class
|
||||||
|
* This is a thin presentation layer over @tm/core
|
||||||
|
*/
|
||||||
|
export class SetStatusCommand extends Command {
|
||||||
|
private tmCore?: TaskMasterCore;
|
||||||
|
private lastResult?: SetStatusResult;
|
||||||
|
|
||||||
|
constructor(name?: string) {
|
||||||
|
super(name || 'set-status');
|
||||||
|
|
||||||
|
// Configure the command
|
||||||
|
this.description('Update the status of one or more tasks')
|
||||||
|
.requiredOption(
|
||||||
|
'-i, --id <id>',
|
||||||
|
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
|
||||||
|
)
|
||||||
|
.requiredOption(
|
||||||
|
'-s, --status <status>',
|
||||||
|
`New status (${VALID_TASK_STATUSES.join(', ')})`
|
||||||
|
)
|
||||||
|
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
||||||
|
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||||
|
.option('-p, --project <path>', 'Project root directory', process.cwd())
|
||||||
|
.action(async (options: SetStatusCommandOptions) => {
|
||||||
|
await this.executeCommand(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the set-status command
|
||||||
|
*/
|
||||||
|
private async executeCommand(
|
||||||
|
options: SetStatusCommandOptions
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Validate required options
|
||||||
|
if (!options.id) {
|
||||||
|
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.status) {
|
||||||
|
console.error(
|
||||||
|
chalk.red('Error: Status is required. Use -s or --status')
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status
|
||||||
|
if (!VALID_TASK_STATUSES.includes(options.status)) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize TaskMaster core
|
||||||
|
this.tmCore = await createTaskMasterCore({
|
||||||
|
projectPath: options.project || process.cwd()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse task IDs (handle comma-separated values)
|
||||||
|
const taskIds = options.id.split(',').map((id) => id.trim());
|
||||||
|
|
||||||
|
// Update each task
|
||||||
|
const updatedTasks: Array<{
|
||||||
|
taskId: string;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const taskId of taskIds) {
|
||||||
|
try {
|
||||||
|
const result = await this.tmCore.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
options.status
|
||||||
|
);
|
||||||
|
updatedTasks.push({
|
||||||
|
taskId: result.taskId,
|
||||||
|
oldStatus: result.oldStatus,
|
||||||
|
newStatus: result.newStatus
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.format === 'json') {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
taskId,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store result for potential reuse
|
||||||
|
this.lastResult = {
|
||||||
|
success: true,
|
||||||
|
updatedTasks,
|
||||||
|
storageType: this.tmCore.getStorageType() as Exclude<
|
||||||
|
StorageType,
|
||||||
|
'auto'
|
||||||
|
>
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
this.displayResults(this.lastResult, options);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.format === 'json') {
|
||||||
|
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
// Clean up resources
|
||||||
|
if (this.tmCore) {
|
||||||
|
await this.tmCore.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display results based on format
|
||||||
|
*/
|
||||||
|
private displayResults(
|
||||||
|
result: SetStatusResult,
|
||||||
|
options: SetStatusCommandOptions
|
||||||
|
): void {
|
||||||
|
const format = options.format || 'text';
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
if (!options.silent) {
|
||||||
|
this.displayTextResults(result);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display results in text format
|
||||||
|
*/
|
||||||
|
private displayTextResults(result: SetStatusResult): void {
|
||||||
|
if (result.updatedTasks.length === 1) {
|
||||||
|
// Single task update
|
||||||
|
const update = result.updatedTasks[0];
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
|
||||||
|
'\n\n' +
|
||||||
|
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
|
||||||
|
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
borderColor: 'green',
|
||||||
|
borderStyle: 'round',
|
||||||
|
margin: { top: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Multiple task updates
|
||||||
|
console.log(
|
||||||
|
boxen(
|
||||||
|
chalk.white.bold(
|
||||||
|
`✅ Successfully updated ${result.updatedTasks.length} tasks`
|
||||||
|
) +
|
||||||
|
'\n\n' +
|
||||||
|
result.updatedTasks
|
||||||
|
.map(
|
||||||
|
(update) =>
|
||||||
|
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
|
||||||
|
)
|
||||||
|
.join('\n'),
|
||||||
|
{
|
||||||
|
padding: 1,
|
||||||
|
borderColor: 'green',
|
||||||
|
borderStyle: 'round',
|
||||||
|
margin: { top: 1 }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show storage info
|
||||||
|
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get colored status display
|
||||||
|
*/
|
||||||
|
private getStatusDisplay(status: TaskStatus): string {
|
||||||
|
const statusColors: Record<TaskStatus, (text: string) => string> = {
|
||||||
|
pending: chalk.yellow,
|
||||||
|
'in-progress': chalk.blue,
|
||||||
|
done: chalk.green,
|
||||||
|
deferred: chalk.gray,
|
||||||
|
cancelled: chalk.red,
|
||||||
|
blocked: chalk.red,
|
||||||
|
review: chalk.magenta,
|
||||||
|
completed: chalk.green
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorFn = statusColors[status] || chalk.white;
|
||||||
|
return colorFn(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last command result (useful for testing or chaining)
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create and configure the set-status command
|
||||||
|
*/
|
||||||
|
export function createSetStatusCommand(): SetStatusCommand {
|
||||||
|
return new SetStatusCommand();
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export { ListTasksCommand } from './commands/list.command.js';
|
|||||||
export { ShowCommand } from './commands/show.command.js';
|
export { ShowCommand } from './commands/show.command.js';
|
||||||
export { AuthCommand } from './commands/auth.command.js';
|
export { AuthCommand } from './commands/auth.command.js';
|
||||||
export { ContextCommand } from './commands/context.command.js';
|
export { ContextCommand } from './commands/context.command.js';
|
||||||
|
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||||
|
|
||||||
// UI utilities (for other commands to use)
|
// UI utilities (for other commands to use)
|
||||||
export * as ui from './utils/ui.js';
|
export * as ui from './utils/ui.js';
|
||||||
|
|||||||
@@ -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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,4 +360,74 @@ export class TaskService {
|
|||||||
async setActiveTag(tag: string): Promise<void> {
|
async setActiveTag(tag: string): Promise<void> {
|
||||||
await this.configManager.setActiveTag(tag);
|
await this.configManager.setActiveTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task status
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(
|
||||||
|
taskId: string | number,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
taskId: string;
|
||||||
|
}> {
|
||||||
|
|
||||||
|
// Ensure we have storage
|
||||||
|
if (!this.storage) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
'Storage not initialized',
|
||||||
|
ERROR_CODES.STORAGE_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided tag or get active tag
|
||||||
|
const activeTag = tag || this.getActiveTag();
|
||||||
|
|
||||||
|
const taskIdStr = String(taskId);
|
||||||
|
|
||||||
|
// 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('.')) {
|
||||||
|
throw new TaskMasterError(
|
||||||
|
'Subtask status updates not yet supported in API storage',
|
||||||
|
ERROR_CODES.NOT_IMPLEMENTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
newStatus,
|
||||||
|
taskId: taskIdStr
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -175,6 +175,22 @@ export class TaskMasterCore {
|
|||||||
await this.configManager.setActiveTag(tag);
|
await this.configManager.setActiveTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update task status
|
||||||
|
*/
|
||||||
|
async updateTaskStatus(
|
||||||
|
taskId: string | number,
|
||||||
|
newStatus: TaskStatus,
|
||||||
|
tag?: string
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
oldStatus: TaskStatus;
|
||||||
|
newStatus: TaskStatus;
|
||||||
|
taskId: string;
|
||||||
|
}> {
|
||||||
|
return this.taskService.updateTaskStatus(taskId, newStatus, tag);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close and cleanup resources
|
* Close and cleanup resources
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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