Compare commits
4 Commits
task-maste
...
task-maste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd90b4d65f | ||
|
|
33259cc4f8 | ||
|
|
1917e6a01a | ||
|
|
35f3e71d2d |
@@ -13,6 +13,7 @@
|
|||||||
],
|
],
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"docs"
|
"docs"
|
||||||
]
|
]
|
||||||
|
|||||||
5
.github/workflows/pre-release.yml
vendored
5
.github/workflows/pre-release.yml
vendored
@@ -65,11 +65,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: Run format
|
|
||||||
run: npm run format
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: npm run turbo:build
|
run: npm run turbo:build
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# @tm/cli
|
|
||||||
|
|
||||||
## 1.1.0-rc.0
|
|
||||||
|
|
||||||
### Minor Changes
|
|
||||||
|
|
||||||
- [#1213](https://github.com/eyaltoledano/claude-task-master/pull/1213) [`cd90b4d`](https://github.com/eyaltoledano/claude-task-master/commit/cd90b4d65fc2f04bdad9fb73aba320b58a124240) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - testing this stuff out to see how the release candidate works with monorepo
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/cli",
|
"name": "@tm/cli",
|
||||||
"version": "0.26.0",
|
"version": "1.0.0",
|
||||||
"description": "Task Master CLI - Command line interface for task management",
|
"description": "Task Master CLI - Command line interface for task management",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ export class ContextCommand extends Command {
|
|||||||
choices: [
|
choices: [
|
||||||
{ name: '(No brief - organization level)', value: null },
|
{ name: '(No brief - organization level)', value: null },
|
||||||
...briefs.map((brief) => ({
|
...briefs.map((brief) => ({
|
||||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
name: `Brief ${brief.id.slice(0, 8)} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||||
value: brief
|
value: brief
|
||||||
}))
|
}))
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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,7 +8,6 @@ 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';
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "*"
|
"task-master-ai": "0.26.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
7263
package-lock.json
generated
7263
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
"dev": "tsdown --watch='packages/*/src/**/*' --watch='apps/cli/src/**/*' --watch='bin/**/*' --watch='mcp-server/**/*'",
|
||||||
"turbo:dev": "turbo dev",
|
"turbo:dev": "turbo dev",
|
||||||
"turbo:build": "turbo build",
|
"turbo:build": "turbo build",
|
||||||
"turbo:typecheck": "turbo typecheck",
|
"turbo:typecheck": "turbo typecheck",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/core",
|
"name": "@tm/core",
|
||||||
"version": "0.26.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Core library for Task Master - TypeScript task management system",
|
"description": "Core library for Task Master - TypeScript task management system",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ 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
|
||||||
@@ -183,7 +175,6 @@ 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
|
||||||
*/
|
*/
|
||||||
static mapStatus(
|
private static mapStatus(
|
||||||
status: Database['public']['Enums']['task_status']
|
status: Database['public']['Enums']['task_status']
|
||||||
): Task['status'] {
|
): Task['status'] {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -3,30 +3,6 @@ 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>) {}
|
||||||
@@ -84,22 +60,12 @@ export class SupabaseTaskRepository {
|
|||||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
async getTask(accountId: 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('brief_id', context.briefId)
|
.eq('account_id', accountId)
|
||||||
.eq('display_id', taskId.toUpperCase())
|
.eq('id', taskId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -141,85 +107,4 @@ 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,74 +360,4 @@ 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,6 +223,14 @@ 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)
|
||||||
);
|
);
|
||||||
@@ -469,7 +477,6 @@ 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,14 +102,6 @@ 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,22 +175,6 @@ 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,9 +1684,63 @@ function registerCommands(programInstance) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the set-status command from @tm/cli
|
// set-status command
|
||||||
// Handles task status updates with proper error handling and validation
|
programInstance
|
||||||
SetStatusCommand.registerOn(programInstance);
|
.command('set-status')
|
||||||
|
.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