feat: initial tm-core pre-cleanup

This commit is contained in:
Ralph Khreish
2025-08-22 14:28:01 +02:00
parent d5c2acc8bf
commit 281f556203
22 changed files with 572 additions and 289 deletions

View File

@@ -8,7 +8,13 @@
}, },
"files": { "files": {
"include": ["src/**/*.ts", "tests/**/*.ts", "*.ts", "*.js", "*.json"], "include": ["src/**/*.ts", "tests/**/*.ts", "*.ts", "*.js", "*.json"],
"ignore": ["**/node_modules", "**/dist", "**/.git", "**/coverage", "**/*.d.ts"] "ignore": [
"**/node_modules",
"**/dist",
"**/.git",
"**/coverage",
"**/*.d.ts"
]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,

View File

@@ -0,0 +1,75 @@
/**
* @fileoverview Constants for Task Master Core
* Single source of truth for all constant values
*/
import type {
TaskStatus,
TaskPriority,
TaskComplexity
} from '../types/index.js';
/**
* Valid task status values
*/
export const TASK_STATUSES: readonly TaskStatus[] = [
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
] as const;
/**
* Valid task priority values
*/
export const TASK_PRIORITIES: readonly TaskPriority[] = [
'low',
'medium',
'high',
'critical'
] as const;
/**
* Valid task complexity values
*/
export const TASK_COMPLEXITIES: readonly TaskComplexity[] = [
'simple',
'moderate',
'complex',
'very-complex'
] as const;
/**
* Valid output formats for task display
*/
export const OUTPUT_FORMATS = ['text', 'json', 'compact'] as const;
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
/**
* Status icons for display
*/
export const STATUS_ICONS: Record<TaskStatus, string> = {
done: '✓',
'in-progress': '►',
blocked: '⭕',
pending: '○',
deferred: '⏸',
cancelled: '✗',
review: '👁'
} as const;
/**
* Status colors for display (using chalk color names)
*/
export const STATUS_COLORS: Record<TaskStatus, string> = {
pending: 'yellow',
'in-progress': 'blue',
done: 'green',
deferred: 'gray',
cancelled: 'red',
blocked: 'magenta',
review: 'cyan'
} as const;

View File

@@ -3,7 +3,12 @@
*/ */
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import type { Subtask, Task, TaskPriority, TaskStatus } from '../types/index.js'; import type {
Subtask,
Task,
TaskPriority,
TaskStatus
} from '../types/index.js';
/** /**
* Task entity representing a task with business logic * Task entity representing a task with business logic
@@ -64,11 +69,17 @@ export class TaskEntity implements Task {
} }
if (!data.title || data.title.trim().length === 0) { if (!data.title || data.title.trim().length === 0) {
throw new TaskMasterError('Task title is required', ERROR_CODES.VALIDATION_ERROR); throw new TaskMasterError(
'Task title is required',
ERROR_CODES.VALIDATION_ERROR
);
} }
if (!data.description || data.description.trim().length === 0) { if (!data.description || data.description.trim().length === 0) {
throw new TaskMasterError('Task description is required', ERROR_CODES.VALIDATION_ERROR); throw new TaskMasterError(
'Task description is required',
ERROR_CODES.VALIDATION_ERROR
);
} }
if (!this.isValidStatus(data.status)) { if (!this.isValidStatus(data.status)) {
@@ -184,7 +195,10 @@ export class TaskEntity implements Task {
*/ */
updateStatus(newStatus: TaskStatus): void { updateStatus(newStatus: TaskStatus): void {
if (!this.isValidStatus(newStatus)) { if (!this.isValidStatus(newStatus)) {
throw new TaskMasterError(`Invalid status: ${newStatus}`, ERROR_CODES.VALIDATION_ERROR); throw new TaskMasterError(
`Invalid status: ${newStatus}`,
ERROR_CODES.VALIDATION_ERROR
);
} }
// Business rule: Cannot move from done to pending // Business rule: Cannot move from done to pending

View File

@@ -215,7 +215,14 @@ export class TaskMasterError extends Error {
private containsSensitiveInfo(obj: any): boolean { private containsSensitiveInfo(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false; if (typeof obj !== 'object' || obj === null) return false;
const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth', 'credential']; const sensitiveKeys = [
'password',
'token',
'key',
'secret',
'auth',
'credential'
];
const objString = JSON.stringify(obj).toLowerCase(); const objString = JSON.stringify(obj).toLowerCase();
return sensitiveKeys.some((key) => objString.includes(key)); return sensitiveKeys.some((key) => objString.includes(key));
@@ -297,7 +304,9 @@ export class TaskMasterError extends Error {
/** /**
* Create a new error with additional context * Create a new error with additional context
*/ */
public withContext(additionalContext: Partial<ErrorContext>): TaskMasterError { public withContext(
additionalContext: Partial<ErrorContext>
): TaskMasterError {
return new TaskMasterError( return new TaskMasterError(
this.message, this.message,
this.code, this.code,

View File

@@ -17,11 +17,19 @@ export type * from './types/index';
// Re-export interfaces (types only to avoid conflicts) // Re-export interfaces (types only to avoid conflicts)
export type * from './interfaces/index'; export type * from './interfaces/index';
// Re-export constants
export * from './constants/index';
// Re-export providers // Re-export providers
export * from './providers/index'; export * from './providers/index';
// Re-export storage (selectively to avoid conflicts) // Re-export storage (selectively to avoid conflicts)
export { FileStorage, ApiStorage, StorageFactory, type ApiStorageConfig } from './storage/index'; export {
FileStorage,
ApiStorage,
StorageFactory,
type ApiStorageConfig
} from './storage/index';
export { PlaceholderStorage, type StorageAdapter } from './storage/index'; export { PlaceholderStorage, type StorageAdapter } from './storage/index';
// Re-export parser // Re-export parser

View File

@@ -282,7 +282,10 @@ export abstract class BaseAIProvider implements IAIProvider {
} }
// Abstract methods that must be implemented by concrete classes // Abstract methods that must be implemented by concrete classes
abstract generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>; abstract generateCompletion(
prompt: string,
options?: AIOptions
): Promise<AIResponse>;
abstract generateStreamingCompletion( abstract generateStreamingCompletion(
prompt: string, prompt: string,
options?: AIOptions options?: AIOptions
@@ -306,7 +309,9 @@ export abstract class BaseAIProvider implements IAIProvider {
const modelExists = availableModels.some((m) => m.id === model); const modelExists = availableModels.some((m) => m.id === model);
if (!modelExists) { if (!modelExists) {
throw new Error(`Model "${model}" is not available for provider "${this.getName()}"`); throw new Error(
`Model "${model}" is not available for provider "${this.getName()}"`
);
} }
this.currentModel = model; this.currentModel = model;
@@ -342,7 +347,11 @@ export abstract class BaseAIProvider implements IAIProvider {
* @param duration - Request duration in milliseconds * @param duration - Request duration in milliseconds
* @param success - Whether the request was successful * @param success - Whether the request was successful
*/ */
protected updateUsageStats(response: AIResponse, duration: number, success: boolean): void { protected updateUsageStats(
response: AIResponse,
duration: number,
success: boolean
): void {
if (!this.usageStats) return; if (!this.usageStats) return;
this.usageStats.totalRequests++; this.usageStats.totalRequests++;
@@ -361,15 +370,18 @@ export abstract class BaseAIProvider implements IAIProvider {
} }
// Update average response time // Update average response time
const totalTime = this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1); const totalTime =
this.usageStats.averageResponseTime = (totalTime + duration) / this.usageStats.totalRequests; this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
this.usageStats.averageResponseTime =
(totalTime + duration) / this.usageStats.totalRequests;
// Update success rate // Update success rate
const successCount = Math.floor( const successCount = Math.floor(
this.usageStats.successRate * (this.usageStats.totalRequests - 1) this.usageStats.successRate * (this.usageStats.totalRequests - 1)
); );
const newSuccessCount = successCount + (success ? 1 : 0); const newSuccessCount = successCount + (success ? 1 : 0);
this.usageStats.successRate = newSuccessCount / this.usageStats.totalRequests; this.usageStats.successRate =
newSuccessCount / this.usageStats.totalRequests;
this.usageStats.lastRequestAt = new Date().toISOString(); this.usageStats.lastRequestAt = new Date().toISOString();
} }

View File

@@ -40,7 +40,11 @@ export interface IStorage {
* @param tag - Optional tag context for the task * @param tag - Optional tag context for the task
* @returns Promise that resolves when update is complete * @returns Promise that resolves when update is complete
*/ */
updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>; updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void>;
/** /**
* Delete a task by ID * Delete a task by ID
@@ -173,7 +177,11 @@ export abstract class BaseStorage implements IStorage {
abstract loadTasks(tag?: string): Promise<Task[]>; abstract loadTasks(tag?: string): Promise<Task[]>;
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(taskId: string, updates: Partial<Task>, tag?: string): Promise<void>; abstract updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void>;
abstract deleteTask(taskId: string, tag?: string): Promise<void>; abstract deleteTask(taskId: string, tag?: string): Promise<void>;
abstract exists(tag?: string): Promise<boolean>; abstract exists(tag?: string): Promise<boolean>;
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>; abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;

View File

@@ -22,7 +22,9 @@ export interface TaskParser {
export class PlaceholderParser implements TaskParser { export class PlaceholderParser implements TaskParser {
async parse(content: string): Promise<PlaceholderTask[]> { async parse(content: string): Promise<PlaceholderTask[]> {
// Simple placeholder parsing logic // Simple placeholder parsing logic
const lines = content.split('\n').filter((line) => line.trim().startsWith('-')); const lines = content
.split('\n')
.filter((line) => line.trim().startsWith('-'));
return lines.map((line, index) => ({ return lines.map((line, index) => ({
id: `task-${index + 1}`, id: `task-${index + 1}`,
title: line.trim().replace(/^-\s*/, ''), title: line.trim().replace(/^-\s*/, ''),

View File

@@ -3,8 +3,15 @@
* Provides common functionality, error handling, and retry logic * Provides common functionality, error handling, and retry logic
*/ */
import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js'; import {
import type { AIOptions, AIResponse, IAIProvider } from '../../interfaces/ai-provider.interface.js'; ERROR_CODES,
TaskMasterError
} from '../../errors/task-master-error.js';
import type {
AIOptions,
AIResponse,
IAIProvider
} from '../../interfaces/ai-provider.interface.js';
// Constants for retry logic // Constants for retry logic
const DEFAULT_MAX_RETRIES = 3; const DEFAULT_MAX_RETRIES = 3;
@@ -67,7 +74,10 @@ export abstract class BaseProvider implements IAIProvider {
constructor(config: BaseProviderConfig) { constructor(config: BaseProviderConfig) {
if (!config.apiKey) { if (!config.apiKey) {
throw new TaskMasterError('API key is required', ERROR_CODES.AUTHENTICATION_ERROR); throw new TaskMasterError(
'API key is required',
ERROR_CODES.AUTHENTICATION_ERROR
);
} }
this.apiKey = config.apiKey; this.apiKey = config.apiKey;
this.model = config.model || this.getDefaultModel(); this.model = config.model || this.getDefaultModel();
@@ -77,11 +87,17 @@ export abstract class BaseProvider implements IAIProvider {
* Template method for generating completions * Template method for generating completions
* Handles validation, retries, and error handling * Handles validation, retries, and error handling
*/ */
async generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse> { async generateCompletion(
prompt: string,
options?: AIOptions
): Promise<AIResponse> {
// Validate input // Validate input
const validation = this.validateInput(prompt, options); const validation = this.validateInput(prompt, options);
if (!validation.valid) { if (!validation.valid) {
throw new TaskMasterError(validation.error || 'Invalid input', ERROR_CODES.VALIDATION_ERROR); throw new TaskMasterError(
validation.error || 'Invalid input',
ERROR_CODES.VALIDATION_ERROR
);
} }
// Prepare request // Prepare request
@@ -94,7 +110,10 @@ export abstract class BaseProvider implements IAIProvider {
for (let attempt = 1; attempt <= maxRetries; attempt++) { for (let attempt = 1; attempt <= maxRetries; attempt++) {
try { try {
const startTime = Date.now(); const startTime = Date.now();
const result = await this.generateCompletionInternal(prepared.prompt, prepared.options); const result = await this.generateCompletionInternal(
prepared.prompt,
prepared.options
);
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
return this.handleResponse(result, duration, prepared); return this.handleResponse(result, duration, prepared);
@@ -117,7 +136,10 @@ export abstract class BaseProvider implements IAIProvider {
/** /**
* Validate input prompt and options * Validate input prompt and options
*/ */
protected validateInput(prompt: string, options?: AIOptions): ValidationResult { protected validateInput(
prompt: string,
options?: AIOptions
): ValidationResult {
// Validate prompt // Validate prompt
if (!prompt || typeof prompt !== 'string') { if (!prompt || typeof prompt !== 'string') {
return { valid: false, error: 'Prompt must be a non-empty string' }; return { valid: false, error: 'Prompt must be a non-empty string' };
@@ -151,7 +173,10 @@ export abstract class BaseProvider implements IAIProvider {
*/ */
protected validateOptions(options: AIOptions): ValidationResult { protected validateOptions(options: AIOptions): ValidationResult {
if (options.temperature !== undefined) { if (options.temperature !== undefined) {
if (options.temperature < MIN_TEMPERATURE || options.temperature > MAX_TEMPERATURE) { if (
options.temperature < MIN_TEMPERATURE ||
options.temperature > MAX_TEMPERATURE
) {
return { return {
valid: false, valid: false,
error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}` error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
@@ -160,7 +185,10 @@ export abstract class BaseProvider implements IAIProvider {
} }
if (options.maxTokens !== undefined) { if (options.maxTokens !== undefined) {
if (options.maxTokens < MIN_MAX_TOKENS || options.maxTokens > MAX_MAX_TOKENS) { if (
options.maxTokens < MIN_MAX_TOKENS ||
options.maxTokens > MAX_MAX_TOKENS
) {
return { return {
valid: false, valid: false,
error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}` error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
@@ -180,7 +208,10 @@ export abstract class BaseProvider implements IAIProvider {
/** /**
* Prepare request for processing * Prepare request for processing
*/ */
protected prepareRequest(prompt: string, options?: AIOptions): PreparedRequest { protected prepareRequest(
prompt: string,
options?: AIOptions
): PreparedRequest {
const defaultOptions = this.getDefaultOptions(); const defaultOptions = this.getDefaultOptions();
const mergedOptions = { ...defaultOptions, ...options }; const mergedOptions = { ...defaultOptions, ...options };
@@ -203,8 +234,10 @@ export abstract class BaseProvider implements IAIProvider {
duration: number, duration: number,
request: PreparedRequest request: PreparedRequest
): AIResponse { ): AIResponse {
const inputTokens = result.inputTokens || this.calculateTokens(request.prompt); const inputTokens =
const outputTokens = result.outputTokens || this.calculateTokens(result.content); result.inputTokens || this.calculateTokens(request.prompt);
const outputTokens =
result.outputTokens || this.calculateTokens(result.content);
return { return {
content: result.content, content: result.content,
@@ -320,7 +353,8 @@ export abstract class BaseProvider implements IAIProvider {
* Calculate exponential backoff delay with jitter * Calculate exponential backoff delay with jitter
*/ */
protected calculateBackoffDelay(attempt: number): number { protected calculateBackoffDelay(attempt: number): number {
const exponentialDelay = BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1); const exponentialDelay =
BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS); const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
// Add jitter to prevent thundering herd // Add jitter to prevent thundering herd
@@ -394,11 +428,16 @@ export abstract class BaseProvider implements IAIProvider {
options?: AIOptions options?: AIOptions
): AsyncIterator<Partial<AIResponse>>; ): AsyncIterator<Partial<AIResponse>>;
abstract isAvailable(): Promise<boolean>; abstract isAvailable(): Promise<boolean>;
abstract getProviderInfo(): import('../../interfaces/ai-provider.interface.js').ProviderInfo; abstract getProviderInfo(): import(
abstract getAvailableModels(): import('../../interfaces/ai-provider.interface.js').AIModel[]; '../../interfaces/ai-provider.interface.js'
).ProviderInfo;
abstract getAvailableModels(): import(
'../../interfaces/ai-provider.interface.js'
).AIModel[];
abstract validateCredentials(): Promise<boolean>; abstract validateCredentials(): Promise<boolean>;
abstract getUsageStats(): Promise< abstract getUsageStats(): Promise<
import('../../interfaces/ai-provider.interface.js').ProviderUsageStats | null | import('../../interfaces/ai-provider.interface.js').ProviderUsageStats
| null
>; >;
abstract initialize(): Promise<void>; abstract initialize(): Promise<void>;
abstract close(): Promise<void>; abstract close(): Promise<void>;

View File

@@ -49,7 +49,7 @@ export class TaskService {
constructor(configManager: ConfigManager) { constructor(configManager: ConfigManager) {
this.configManager = configManager; this.configManager = configManager;
// Storage will be created during initialization // Storage will be created during initialization
this.storage = null as any; this.storage = null as any;
} }
@@ -66,7 +66,7 @@ export class TaskService {
// Create storage based on configuration // Create storage based on configuration
const storageConfig = this.configManager.getStorageConfig(); const storageConfig = this.configManager.getStorageConfig();
const projectRoot = this.configManager.getProjectRoot(); const projectRoot = this.configManager.getProjectRoot();
this.storage = StorageFactory.create( this.storage = StorageFactory.create(
{ storage: storageConfig } as any, { storage: storageConfig } as any,
projectRoot projectRoot
@@ -74,7 +74,7 @@ export class TaskService {
// Initialize storage // Initialize storage
await this.storage.initialize(); await this.storage.initialize();
this.initialized = true; this.initialized = true;
} }
@@ -103,11 +103,11 @@ export class TaskService {
} }
// Convert back to plain objects // Convert back to plain objects
let tasks = filteredEntities.map(entity => entity.toJSON()); let tasks = filteredEntities.map((entity) => entity.toJSON());
// Handle subtasks option // Handle subtasks option
if (options.includeSubtasks === false) { if (options.includeSubtasks === false) {
tasks = tasks.map(task => ({ tasks = tasks.map((task) => ({
...task, ...task,
subtasks: [] subtasks: []
})); }));
@@ -124,7 +124,7 @@ export class TaskService {
throw new TaskMasterError( throw new TaskMasterError(
'Failed to get task list', 'Failed to get task list',
ERROR_CODES.INTERNAL_ERROR, ERROR_CODES.INTERNAL_ERROR,
{ {
operation: 'getTaskList', operation: 'getTaskList',
tag, tag,
hasFilter: !!options.filter hasFilter: !!options.filter
@@ -138,28 +138,28 @@ export class TaskService {
* Get a single task by ID * Get a single task by ID
*/ */
async getTask(taskId: string, tag?: string): Promise<Task | null> { async getTask(taskId: string, tag?: string): Promise<Task | null> {
const result = await this.getTaskList({ const result = await this.getTaskList({
tag, tag,
includeSubtasks: true includeSubtasks: true
}); });
return result.tasks.find(t => t.id === taskId) || null; return result.tasks.find((t) => t.id === taskId) || null;
} }
/** /**
* Get tasks filtered by status * Get tasks filtered by status
*/ */
async getTasksByStatus( async getTasksByStatus(
status: TaskStatus | TaskStatus[], status: TaskStatus | TaskStatus[],
tag?: string tag?: string
): Promise<Task[]> { ): Promise<Task[]> {
const statuses = Array.isArray(status) ? status : [status]; const statuses = Array.isArray(status) ? status : [status];
const result = await this.getTaskList({ const result = await this.getTaskList({
tag, tag,
filter: { status: statuses } filter: { status: statuses }
}); });
return result.tasks; return result.tasks;
} }
@@ -173,9 +173,9 @@ export class TaskService {
blocked: number; blocked: number;
storageType: 'file' | 'api'; storageType: 'file' | 'api';
}> { }> {
const result = await this.getTaskList({ const result = await this.getTaskList({
tag, tag,
includeSubtasks: true includeSubtasks: true
}); });
const stats = { const stats = {
@@ -188,22 +188,27 @@ export class TaskService {
// Initialize all statuses // Initialize all statuses
const allStatuses: TaskStatus[] = [ const allStatuses: TaskStatus[] = [
'pending', 'in-progress', 'done', 'pending',
'deferred', 'cancelled', 'blocked', 'review' 'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
]; ];
allStatuses.forEach(status => { allStatuses.forEach((status) => {
stats.byStatus[status] = 0; stats.byStatus[status] = 0;
}); });
// Count tasks // Count tasks
result.tasks.forEach(task => { result.tasks.forEach((task) => {
stats.byStatus[task.status]++; stats.byStatus[task.status]++;
if (task.subtasks && task.subtasks.length > 0) { if (task.subtasks && task.subtasks.length > 0) {
stats.withSubtasks++; stats.withSubtasks++;
} }
if (task.status === 'blocked') { if (task.status === 'blocked') {
stats.blocked++; stats.blocked++;
} }
@@ -225,21 +230,19 @@ export class TaskService {
// Find tasks with no dependencies or all dependencies satisfied // Find tasks with no dependencies or all dependencies satisfied
const completedIds = new Set( const completedIds = new Set(
result.tasks result.tasks.filter((t) => t.status === 'done').map((t) => t.id)
.filter(t => t.status === 'done')
.map(t => t.id)
); );
const availableTasks = result.tasks.filter(task => { const availableTasks = result.tasks.filter((task) => {
if (task.status === 'done' || task.status === 'blocked') { if (task.status === 'done' || task.status === 'blocked') {
return false; return false;
} }
if (!task.dependencies || task.dependencies.length === 0) { if (!task.dependencies || task.dependencies.length === 0) {
return true; return true;
} }
return task.dependencies.every(depId => return task.dependencies.every((depId) =>
completedIds.has(depId.toString()) completedIds.has(depId.toString())
); );
}); });
@@ -259,10 +262,12 @@ export class TaskService {
* Apply filters to task entities * Apply filters to task entities
*/ */
private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] { private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
return tasks.filter(task => { return tasks.filter((task) => {
// Status filter // Status filter
if (filter.status) { if (filter.status) {
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status]; const statuses = Array.isArray(filter.status)
? filter.status
: [filter.status];
if (!statuses.includes(task.status)) { if (!statuses.includes(task.status)) {
return false; return false;
} }
@@ -270,7 +275,9 @@ export class TaskService {
// Priority filter // Priority filter
if (filter.priority) { if (filter.priority) {
const priorities = Array.isArray(filter.priority) ? filter.priority : [filter.priority]; const priorities = Array.isArray(filter.priority)
? filter.priority
: [filter.priority];
if (!priorities.includes(task.priority)) { if (!priorities.includes(task.priority)) {
return false; return false;
} }
@@ -278,7 +285,10 @@ export class TaskService {
// Tags filter // Tags filter
if (filter.tags && filter.tags.length > 0) { if (filter.tags && filter.tags.length > 0) {
if (!task.tags || !filter.tags.some(tag => task.tags?.includes(tag))) { if (
!task.tags ||
!filter.tags.some((tag) => task.tags?.includes(tag))
) {
return false; return false;
} }
} }
@@ -292,8 +302,8 @@ export class TaskService {
// Complexity filter // Complexity filter
if (filter.complexity) { if (filter.complexity) {
const complexities = Array.isArray(filter.complexity) const complexities = Array.isArray(filter.complexity)
? filter.complexity ? filter.complexity
: [filter.complexity]; : [filter.complexity];
if (!task.complexity || !complexities.includes(task.complexity)) { if (!task.complexity || !complexities.includes(task.complexity)) {
return false; return false;
@@ -304,9 +314,11 @@ export class TaskService {
if (filter.search) { if (filter.search) {
const searchLower = filter.search.toLowerCase(); const searchLower = filter.search.toLowerCase();
const inTitle = task.title.toLowerCase().includes(searchLower); const inTitle = task.title.toLowerCase().includes(searchLower);
const inDescription = task.description.toLowerCase().includes(searchLower); const inDescription = task.description
.toLowerCase()
.includes(searchLower);
const inDetails = task.details.toLowerCase().includes(searchLower); const inDetails = task.details.toLowerCase().includes(searchLower);
if (!inTitle && !inDescription && !inDetails) { if (!inTitle && !inDescription && !inDetails) {
return false; return false;
} }
@@ -353,4 +365,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);
} }
} }

View File

@@ -3,7 +3,10 @@
* This provides storage via REST API instead of local file system * This provides storage via REST API instead of local file system
*/ */
import type { IStorage, StorageStats } from '../interfaces/storage.interface.js'; import type {
IStorage,
StorageStats
} from '../interfaces/storage.interface.js';
import type { Task, TaskMetadata } from '../types/index.js'; import type { Task, TaskMetadata } from '../types/index.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
@@ -45,7 +48,7 @@ export class ApiStorage implements IStorage {
constructor(config: ApiStorageConfig) { constructor(config: ApiStorageConfig) {
this.validateConfig(config); this.validateConfig(config);
this.config = { this.config = {
endpoint: config.endpoint.replace(/\/$/, ''), // Remove trailing slash endpoint: config.endpoint.replace(/\/$/, ''), // Remove trailing slash
accessToken: config.accessToken, accessToken: config.accessToken,
@@ -111,7 +114,7 @@ export class ApiStorage implements IStorage {
*/ */
private async verifyConnection(): Promise<void> { private async verifyConnection(): Promise<void> {
const response = await this.makeRequest<{ status: string }>('/health'); const response = await this.makeRequest<{ status: string }>('/health');
if (!response.success) { if (!response.success) {
throw new Error(`API health check failed: ${response.error}`); throw new Error(`API health check failed: ${response.error}`);
} }
@@ -124,7 +127,7 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
const endpoint = tag const endpoint = tag
? `/projects/${this.config.projectId}/tasks?tag=${encodeURIComponent(tag)}` ? `/projects/${this.config.projectId}/tasks?tag=${encodeURIComponent(tag)}`
: `/projects/${this.config.projectId}/tasks`; : `/projects/${this.config.projectId}/tasks`;
@@ -291,7 +294,9 @@ export class ApiStorage implements IStorage {
? `/projects/${this.config.projectId}/metadata?tag=${encodeURIComponent(tag)}` ? `/projects/${this.config.projectId}/metadata?tag=${encodeURIComponent(tag)}`
: `/projects/${this.config.projectId}/metadata`; : `/projects/${this.config.projectId}/metadata`;
const response = await this.makeRequest<{ metadata: TaskMetadata }>(endpoint); const response = await this.makeRequest<{ metadata: TaskMetadata }>(
endpoint
);
if (!response.success) { if (!response.success) {
return null; return null;
@@ -355,10 +360,10 @@ export class ApiStorage implements IStorage {
try { try {
// First load existing tasks // First load existing tasks
const existingTasks = await this.loadTasks(tag); const existingTasks = await this.loadTasks(tag);
// Append new tasks // Append new tasks
const allTasks = [...existingTasks, ...tasks]; const allTasks = [...existingTasks, ...tasks];
// Save all tasks // Save all tasks
await this.saveTasks(allTasks, tag); await this.saveTasks(allTasks, tag);
} catch (error) { } catch (error) {
@@ -374,20 +379,24 @@ export class ApiStorage implements IStorage {
/** /**
* Update a specific task * Update a specific task
*/ */
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void> { async updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void> {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
// Load the task // Load the task
const task = await this.loadTask(taskId, tag); const task = await this.loadTask(taskId, tag);
if (!task) { if (!task) {
throw new Error(`Task ${taskId} not found`); throw new Error(`Task ${taskId} not found`);
} }
// Merge updates // Merge updates
const updatedTask = { ...task, ...updates, id: taskId }; const updatedTask = { ...task, ...updates, id: taskId };
// Save updated task // Save updated task
await this.saveTask(updatedTask, tag); await this.saveTask(updatedTask, tag);
} catch (error) { } catch (error) {
@@ -500,13 +509,15 @@ export class ApiStorage implements IStorage {
} }
// Return stats or default values // Return stats or default values
return response.data?.stats || { return (
totalTasks: 0, response.data?.stats || {
totalTags: 0, totalTasks: 0,
storageSize: 0, totalTags: 0,
lastModified: new Date().toISOString(), storageSize: 0,
tagStats: [] lastModified: new Date().toISOString(),
}; tagStats: []
}
);
} catch (error) { } catch (error) {
throw new TaskMasterError( throw new TaskMasterError(
'Failed to get stats from API', 'Failed to get stats from API',
@@ -627,9 +638,9 @@ export class ApiStorage implements IStorage {
const options: RequestInit = { const options: RequestInit = {
method, method,
headers: { headers: {
'Authorization': `Bearer ${this.config.accessToken}`, Authorization: `Bearer ${this.config.accessToken}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' Accept: 'application/json'
}, },
signal: controller.signal signal: controller.signal
}; };
@@ -654,16 +665,16 @@ export class ApiStorage implements IStorage {
// Handle specific error codes // Handle specific error codes
if (response.status === 401) { if (response.status === 401) {
return { return {
success: false, success: false,
error: 'Authentication failed - check access token' error: 'Authentication failed - check access token'
}; };
} }
if (response.status === 404) { if (response.status === 404) {
return { return {
success: false, success: false,
error: 'Resource not found' error: 'Resource not found'
}; };
} }
@@ -678,7 +689,10 @@ export class ApiStorage implements IStorage {
const errorData = data as any; const errorData = data as any;
return { return {
success: false, success: false,
error: errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}` error:
errorData.error ||
errorData.message ||
`HTTP ${response.status}: ${response.statusText}`
}; };
} catch (error) { } catch (error) {
lastError = error as Error; lastError = error as Error;
@@ -705,6 +719,6 @@ export class ApiStorage implements IStorage {
* Delay helper for retries * Delay helper for retries
*/ */
private delay(ms: number): Promise<void> { private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
} }

View File

@@ -5,7 +5,10 @@
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import type { Task, TaskMetadata } from '../types/index.js'; import type { Task, TaskMetadata } from '../types/index.js';
import type { IStorage, StorageStats } from '../interfaces/storage.interface.js'; import type {
IStorage,
StorageStats
} from '../interfaces/storage.interface.js';
/** /**
* File storage data structure * File storage data structure
@@ -80,7 +83,7 @@ export class FileStorage implements IStorage {
totalTags: tags.length, totalTags: tags.length,
lastModified: lastModified || new Date().toISOString(), lastModified: lastModified || new Date().toISOString(),
storageSize: 0, // Could calculate actual file sizes if needed storageSize: 0, // Could calculate actual file sizes if needed
tagStats: tags.map(tag => ({ tagStats: tags.map((tag) => ({
tag, tag,
taskCount: 0, // Would need to load each tag to get accurate count taskCount: 0, // Would need to load each tag to get accurate count
lastModified: lastModified || new Date().toISOString() lastModified: lastModified || new Date().toISOString()
@@ -218,14 +221,18 @@ export class FileStorage implements IStorage {
/** /**
* Update a specific task * Update a specific task
*/ */
async updateTask(taskId: string, updates: Partial<Task>, tag?: string): Promise<void> { async updateTask(
taskId: string,
updates: Partial<Task>,
tag?: string
): Promise<void> {
const tasks = await this.loadTasks(tag); const tasks = await this.loadTasks(tag);
const taskIndex = tasks.findIndex(t => t.id === taskId); const taskIndex = tasks.findIndex((t) => t.id === taskId);
if (taskIndex === -1) { if (taskIndex === -1) {
throw new Error(`Task ${taskId} not found`); throw new Error(`Task ${taskId} not found`);
} }
tasks[taskIndex] = { ...tasks[taskIndex], ...updates, id: taskId }; tasks[taskIndex] = { ...tasks[taskIndex], ...updates, id: taskId };
await this.saveTasks(tasks, tag); await this.saveTasks(tasks, tag);
} }
@@ -235,12 +242,12 @@ export class FileStorage implements IStorage {
*/ */
async deleteTask(taskId: string, tag?: string): Promise<void> { async deleteTask(taskId: string, tag?: string): Promise<void> {
const tasks = await this.loadTasks(tag); const tasks = await this.loadTasks(tag);
const filteredTasks = tasks.filter(t => t.id !== taskId); const filteredTasks = tasks.filter((t) => t.id !== taskId);
if (filteredTasks.length === tasks.length) { if (filteredTasks.length === tasks.length) {
throw new Error(`Task ${taskId} not found`); throw new Error(`Task ${taskId} not found`);
} }
await this.saveTasks(filteredTasks, tag); await this.saveTasks(filteredTasks, tag);
} }
@@ -264,11 +271,13 @@ export class FileStorage implements IStorage {
async renameTag(oldTag: string, newTag: string): Promise<void> { async renameTag(oldTag: string, newTag: string): Promise<void> {
const oldPath = this.getTasksPath(oldTag); const oldPath = this.getTasksPath(oldTag);
const newPath = this.getTasksPath(newTag); const newPath = this.getTasksPath(newTag);
try { try {
await fs.rename(oldPath, newPath); await fs.rename(oldPath, newPath);
} catch (error: any) { } catch (error: any) {
throw new Error(`Failed to rename tag from ${oldTag} to ${newTag}: ${error.message}`); throw new Error(
`Failed to rename tag from ${oldTag} to ${newTag}: ${error.message}`
);
} }
} }
@@ -278,7 +287,7 @@ export class FileStorage implements IStorage {
async copyTag(sourceTag: string, targetTag: string): Promise<void> { async copyTag(sourceTag: string, targetTag: string): Promise<void> {
const tasks = await this.loadTasks(sourceTag); const tasks = await this.loadTasks(sourceTag);
const metadata = await this.loadMetadata(sourceTag); const metadata = await this.loadMetadata(sourceTag);
await this.saveTasks(tasks, targetTag); await this.saveTasks(tasks, targetTag);
if (metadata) { if (metadata) {
await this.saveMetadata(metadata, targetTag); await this.saveMetadata(metadata, targetTag);
@@ -323,7 +332,9 @@ export class FileStorage implements IStorage {
/** /**
* Read and parse JSON file with error handling * Read and parse JSON file with error handling
*/ */
private async readJsonFile(filePath: string): Promise<FileStorageData | null> { private async readJsonFile(
filePath: string
): Promise<FileStorageData | null> {
try { try {
const content = await fs.readFile(filePath, 'utf-8'); const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content); return JSON.parse(content);
@@ -341,7 +352,10 @@ export class FileStorage implements IStorage {
/** /**
* Write JSON file with atomic operation using temp file * Write JSON file with atomic operation using temp file
*/ */
private async writeJsonFile(filePath: string, data: FileStorageData): Promise<void> { private async writeJsonFile(
filePath: string,
data: FileStorageData
): Promise<void> {
// Use file locking to prevent concurrent writes // Use file locking to prevent concurrent writes
const lockKey = filePath; const lockKey = filePath;
const existingLock = this.fileLocks.get(lockKey); const existingLock = this.fileLocks.get(lockKey);
@@ -363,7 +377,10 @@ export class FileStorage implements IStorage {
/** /**
* Perform the actual write operation * Perform the actual write operation
*/ */
private async performWrite(filePath: string, data: FileStorageData): Promise<void> { private async performWrite(
filePath: string,
data: FileStorageData
): Promise<void> {
const tempPath = `${filePath}.tmp`; const tempPath = `${filePath}.tmp`;
try { try {
@@ -406,11 +423,11 @@ export class FileStorage implements IStorage {
try { try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = this.getBackupPath(filePath, timestamp); const backupPath = this.getBackupPath(filePath, timestamp);
// Ensure backup directory exists // Ensure backup directory exists
const backupDir = path.dirname(backupPath); const backupDir = path.dirname(backupPath);
await fs.mkdir(backupDir, { recursive: true }); await fs.mkdir(backupDir, { recursive: true });
await fs.copyFile(filePath, backupPath); await fs.copyFile(filePath, backupPath);
// Clean up old backups if needed // Clean up old backups if needed
@@ -432,7 +449,9 @@ export class FileStorage implements IStorage {
try { try {
const files = await fs.readdir(dir); const files = await fs.readdir(dir);
const backupFiles = files const backupFiles = files
.filter((f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')) .filter(
(f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')
)
.sort() .sort()
.reverse(); .reverse();

View File

@@ -9,7 +9,10 @@ export { ApiStorage, type ApiStorageConfig } from './api-storage.js';
export { StorageFactory } from './storage-factory.js'; export { StorageFactory } from './storage-factory.js';
// Export storage interface and types // Export storage interface and types
export type { IStorage, StorageStats } from '../interfaces/storage.interface.js'; export type {
IStorage,
StorageStats
} from '../interfaces/storage.interface.js';
// Placeholder exports - these will be implemented in later tasks // Placeholder exports - these will be implemented in later tasks
export interface StorageAdapter { export interface StorageAdapter {

View File

@@ -2,169 +2,169 @@
* @fileoverview Storage factory for creating appropriate storage implementations * @fileoverview Storage factory for creating appropriate storage implementations
*/ */
import type { IStorage } from "../interfaces/storage.interface.js"; import type { IStorage } from '../interfaces/storage.interface.js';
import type { IConfiguration } from "../interfaces/configuration.interface.js"; import type { IConfiguration } from '../interfaces/configuration.interface.js';
import { FileStorage } from "./file-storage.js"; import { FileStorage } from './file-storage.js';
import { ApiStorage } from "./api-storage.js"; import { ApiStorage } from './api-storage.js';
import { ERROR_CODES, TaskMasterError } from "../errors/task-master-error.js"; import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
/** /**
* Factory for creating storage implementations based on configuration * Factory for creating storage implementations based on configuration
*/ */
export class StorageFactory { export class StorageFactory {
/** /**
* Create a storage implementation based on configuration * Create a storage implementation based on configuration
* @param config - Configuration object * @param config - Configuration object
* @param projectPath - Project root path (for file storage) * @param projectPath - Project root path (for file storage)
* @returns Storage implementation * @returns Storage implementation
*/ */
static create( static create(
config: Partial<IConfiguration>, config: Partial<IConfiguration>,
projectPath: string projectPath: string
): IStorage { ): IStorage {
const storageType = config.storage?.type || "file"; const storageType = config.storage?.type || 'file';
switch (storageType) { switch (storageType) {
case "file": case 'file':
return StorageFactory.createFileStorage(projectPath, config); return StorageFactory.createFileStorage(projectPath, config);
case "api": case 'api':
return StorageFactory.createApiStorage(config); return StorageFactory.createApiStorage(config);
default: default:
throw new TaskMasterError( throw new TaskMasterError(
`Unknown storage type: ${storageType}`, `Unknown storage type: ${storageType}`,
ERROR_CODES.INVALID_INPUT, ERROR_CODES.INVALID_INPUT,
{ storageType } { storageType }
); );
} }
} }
/** /**
* Create file storage implementation * Create file storage implementation
*/ */
private static createFileStorage( private static createFileStorage(
projectPath: string, projectPath: string,
config: Partial<IConfiguration> config: Partial<IConfiguration>
): FileStorage { ): FileStorage {
const basePath = config.storage?.basePath || projectPath; const basePath = config.storage?.basePath || projectPath;
return new FileStorage(basePath); return new FileStorage(basePath);
} }
/** /**
* Create API storage implementation * Create API storage implementation
*/ */
private static createApiStorage(config: Partial<IConfiguration>): ApiStorage { private static createApiStorage(config: Partial<IConfiguration>): ApiStorage {
const { apiEndpoint, apiAccessToken } = config.storage || {}; const { apiEndpoint, apiAccessToken } = config.storage || {};
if (!apiEndpoint) { if (!apiEndpoint) {
throw new TaskMasterError( throw new TaskMasterError(
"API endpoint is required for API storage", 'API endpoint is required for API storage',
ERROR_CODES.MISSING_CONFIGURATION, ERROR_CODES.MISSING_CONFIGURATION,
{ storageType: "api" } { storageType: 'api' }
); );
} }
if (!apiAccessToken) { if (!apiAccessToken) {
throw new TaskMasterError( throw new TaskMasterError(
"API access token is required for API storage", 'API access token is required for API storage',
ERROR_CODES.MISSING_CONFIGURATION, ERROR_CODES.MISSING_CONFIGURATION,
{ storageType: "api" } { storageType: 'api' }
); );
} }
return new ApiStorage({ return new ApiStorage({
endpoint: apiEndpoint, endpoint: apiEndpoint,
accessToken: apiAccessToken, accessToken: apiAccessToken,
projectId: config.projectPath, projectId: config.projectPath,
timeout: config.retry?.requestTimeout, timeout: config.retry?.requestTimeout,
enableRetry: config.retry?.retryOnNetworkError, enableRetry: config.retry?.retryOnNetworkError,
maxRetries: config.retry?.retryAttempts, maxRetries: config.retry?.retryAttempts
}); });
} }
/** /**
* Detect optimal storage type based on available configuration * Detect optimal storage type based on available configuration
*/ */
static detectOptimalStorage(config: Partial<IConfiguration>): "file" | "api" { static detectOptimalStorage(config: Partial<IConfiguration>): 'file' | 'api' {
// If API credentials are provided, prefer API storage (Hamster) // If API credentials are provided, prefer API storage (Hamster)
if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) { if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) {
return "api"; return 'api';
} }
// Default to file storage // Default to file storage
return "file"; return 'file';
} }
/** /**
* Validate storage configuration * Validate storage configuration
*/ */
static validateStorageConfig(config: Partial<IConfiguration>): { static validateStorageConfig(config: Partial<IConfiguration>): {
isValid: boolean; isValid: boolean;
errors: string[]; errors: string[];
} { } {
const errors: string[] = []; const errors: string[] = [];
const storageType = config.storage?.type; const storageType = config.storage?.type;
if (!storageType) { if (!storageType) {
errors.push("Storage type is not specified"); errors.push('Storage type is not specified');
return { isValid: false, errors }; return { isValid: false, errors };
} }
switch (storageType) { switch (storageType) {
case "api": case 'api':
if (!config.storage?.apiEndpoint) { if (!config.storage?.apiEndpoint) {
errors.push("API endpoint is required for API storage"); errors.push('API endpoint is required for API storage');
} }
if (!config.storage?.apiAccessToken) { if (!config.storage?.apiAccessToken) {
errors.push("API access token is required for API storage"); errors.push('API access token is required for API storage');
} }
break; break;
case "file": case 'file':
// File storage doesn't require additional config // File storage doesn't require additional config
break; break;
default: default:
errors.push(`Unknown storage type: ${storageType}`); errors.push(`Unknown storage type: ${storageType}`);
} }
return { return {
isValid: errors.length === 0, isValid: errors.length === 0,
errors, errors
}; };
} }
/** /**
* Check if Hamster (API storage) is available * Check if Hamster (API storage) is available
*/ */
static isHamsterAvailable(config: Partial<IConfiguration>): boolean { static isHamsterAvailable(config: Partial<IConfiguration>): boolean {
return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken); return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken);
} }
/** /**
* Create a storage implementation with fallback * Create a storage implementation with fallback
* Tries API storage first, falls back to file storage * Tries API storage first, falls back to file storage
*/ */
static async createWithFallback( static async createWithFallback(
config: Partial<IConfiguration>, config: Partial<IConfiguration>,
projectPath: string projectPath: string
): Promise<IStorage> { ): Promise<IStorage> {
// Try API storage if configured // Try API storage if configured
if (StorageFactory.isHamsterAvailable(config)) { if (StorageFactory.isHamsterAvailable(config)) {
try { try {
const apiStorage = StorageFactory.createApiStorage(config); const apiStorage = StorageFactory.createApiStorage(config);
await apiStorage.initialize(); await apiStorage.initialize();
return apiStorage; return apiStorage;
} catch (error) { } catch (error) {
console.warn( console.warn(
"Failed to initialize API storage, falling back to file storage:", 'Failed to initialize API storage, falling back to file storage:',
error error
); );
} }
} }
// Fallback to file storage // Fallback to file storage
return StorageFactory.createFileStorage(projectPath, config); return StorageFactory.createFileStorage(projectPath, config);
} }
} }

View File

@@ -3,7 +3,11 @@
*/ */
import { ConfigManager } from './config/config-manager.js'; import { ConfigManager } from './config/config-manager.js';
import { TaskService, type TaskListResult as ListTasksResult, type GetTaskListOptions } from './services/task-service.js'; import {
TaskService,
type TaskListResult as ListTasksResult,
type GetTaskListOptions
} from './services/task-service.js';
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js'; import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
import type { IConfiguration } from './interfaces/configuration.interface.js'; import type { IConfiguration } from './interfaces/configuration.interface.js';
import type { Task, TaskStatus, TaskFilter } from './types/index.js'; import type { Task, TaskStatus, TaskFilter } from './types/index.js';
@@ -33,7 +37,10 @@ export class TaskMasterCore {
constructor(options: TaskMasterCoreOptions) { constructor(options: TaskMasterCoreOptions) {
if (!options.projectPath) { if (!options.projectPath) {
throw new TaskMasterError('Project path is required', ERROR_CODES.MISSING_CONFIGURATION); throw new TaskMasterError(
'Project path is required',
ERROR_CODES.MISSING_CONFIGURATION
);
} }
// Create config manager // Create config manager
@@ -108,7 +115,10 @@ export class TaskMasterCore {
/** /**
* Get tasks by status * Get tasks by status
*/ */
async getTasksByStatus(status: TaskStatus | TaskStatus[], tag?: string): Promise<Task[]> { async getTasksByStatus(
status: TaskStatus | TaskStatus[],
tag?: string
): Promise<Task[]> {
await this.ensureInitialized(); await this.ensureInitialized();
return this.taskService.getTasksByStatus(status, tag); return this.taskService.getTasksByStatus(status, tag);
} }

View File

@@ -103,7 +103,10 @@ export interface TaskCollection {
/** /**
* Type for creating a new task (without generated fields) * Type for creating a new task (without generated fields)
*/ */
export type CreateTask = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'subtasks'> & { export type CreateTask = Omit<
Task,
'id' | 'createdAt' | 'updatedAt' | 'subtasks'
> & {
subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[]; subtasks?: Omit<Subtask, 'id' | 'parentId' | 'createdAt' | 'updatedAt'>[];
}; };
@@ -145,7 +148,15 @@ export interface TaskSortOptions {
export function isTaskStatus(value: unknown): value is TaskStatus { export function isTaskStatus(value: unknown): value is TaskStatus {
return ( return (
typeof value === 'string' && typeof value === 'string' &&
['pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review'].includes(value) [
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
].includes(value)
); );
} }
@@ -153,7 +164,10 @@ export function isTaskStatus(value: unknown): value is TaskStatus {
* Type guard to check if a value is a valid TaskPriority * Type guard to check if a value is a valid TaskPriority
*/ */
export function isTaskPriority(value: unknown): value is TaskPriority { export function isTaskPriority(value: unknown): value is TaskPriority {
return typeof value === 'string' && ['low', 'medium', 'high', 'critical'].includes(value); return (
typeof value === 'string' &&
['low', 'medium', 'high', 'critical'].includes(value)
);
} }
/** /**
@@ -161,7 +175,8 @@ export function isTaskPriority(value: unknown): value is TaskPriority {
*/ */
export function isTaskComplexity(value: unknown): value is TaskComplexity { export function isTaskComplexity(value: unknown): value is TaskComplexity {
return ( return (
typeof value === 'string' && ['simple', 'moderate', 'complex', 'very-complex'].includes(value) typeof value === 'string' &&
['simple', 'moderate', 'complex', 'very-complex'].includes(value)
); );
} }

View File

@@ -33,9 +33,14 @@ export function generateTaskId(): string {
* // Returns: "TASK-123-A7B3.2" * // Returns: "TASK-123-A7B3.2"
* ``` * ```
*/ */
export function generateSubtaskId(parentId: string, existingSubtasks: string[] = []): string { export function generateSubtaskId(
parentId: string,
existingSubtasks: string[] = []
): string {
// Find existing subtasks for this parent // Find existing subtasks for this parent
const parentSubtasks = existingSubtasks.filter((id) => id.startsWith(`${parentId}.`)); const parentSubtasks = existingSubtasks.filter((id) =>
id.startsWith(`${parentId}.`)
);
// Extract sequential numbers and find the highest // Extract sequential numbers and find the highest
const sequentialNumbers = parentSubtasks const sequentialNumbers = parentSubtasks
@@ -48,7 +53,8 @@ export function generateSubtaskId(parentId: string, existingSubtasks: string[] =
.sort((a, b) => a - b); .sort((a, b) => a - b);
// Determine the next sequential number // Determine the next sequential number
const nextSequential = sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1; const nextSequential =
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
return `${parentId}.${nextSequential}`; return `${parentId}.${nextSequential}`;
} }

View File

@@ -323,7 +323,9 @@ describe('TaskMasterCore - listTasks E2E', () => {
it('should validate task entities', async () => { it('should validate task entities', async () => {
// Write invalid task data // Write invalid task data
const invalidDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-invalid-')); const invalidDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'tm-invalid-')
);
const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks'); const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
await fs.mkdir(tasksDir, { recursive: true }); await fs.mkdir(tasksDir, { recursive: true });
@@ -349,7 +351,10 @@ describe('TaskMasterCore - listTasks E2E', () => {
} }
}; };
await fs.writeFile(path.join(tasksDir, 'tasks.json'), JSON.stringify(invalidData)); await fs.writeFile(
path.join(tasksDir, 'tasks.json'),
JSON.stringify(invalidData)
);
const invalidCore = createTaskMasterCore(invalidDir); const invalidCore = createTaskMasterCore(invalidDir);
@@ -379,7 +384,12 @@ describe('TaskMasterCore - listTasks E2E', () => {
} }
]; ];
const tagFile = path.join(tmpDir, '.taskmaster', 'tasks', 'feature-branch.json'); const tagFile = path.join(
tmpDir,
'.taskmaster',
'tasks',
'feature-branch.json'
);
await fs.writeFile( await fs.writeFile(
tagFile, tagFile,
JSON.stringify({ JSON.stringify({

View File

@@ -58,7 +58,10 @@ export class MockProvider extends BaseProvider {
throw new Error('Mock provider error'); throw new Error('Mock provider error');
} }
if (this.options.failAfterAttempts && this.attemptCount <= this.options.failAfterAttempts) { if (
this.options.failAfterAttempts &&
this.attemptCount <= this.options.failAfterAttempts
) {
if (this.options.simulateRateLimit) { if (this.options.simulateRateLimit) {
throw new Error('Rate limit exceeded - too many requests (429)'); throw new Error('Rate limit exceeded - too many requests (429)');
} }
@@ -200,6 +203,8 @@ export class MockProvider extends BaseProvider {
// Override retry configuration for testing // Override retry configuration for testing
protected getMaxRetries(): number { protected getMaxRetries(): number {
return this.options.failAfterAttempts ? this.options.failAfterAttempts + 1 : 3; return this.options.failAfterAttempts
? this.options.failAfterAttempts + 1
: 3;
} }
} }

View File

@@ -3,7 +3,10 @@
*/ */
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { ERROR_CODES, TaskMasterError } from '../../src/errors/task-master-error'; import {
ERROR_CODES,
TaskMasterError
} from '../../src/errors/task-master-error';
import { MockProvider } from '../mocks/mock-provider'; import { MockProvider } from '../mocks/mock-provider';
describe('BaseProvider', () => { describe('BaseProvider', () => {
@@ -64,21 +67,21 @@ describe('BaseProvider', () => {
}); });
it('should validate temperature range', async () => { it('should validate temperature range', async () => {
await expect(provider.generateCompletion('Test', { temperature: 3 })).rejects.toThrow( await expect(
'Temperature must be between 0 and 2' provider.generateCompletion('Test', { temperature: 3 })
); ).rejects.toThrow('Temperature must be between 0 and 2');
}); });
it('should validate maxTokens range', async () => { it('should validate maxTokens range', async () => {
await expect(provider.generateCompletion('Test', { maxTokens: 0 })).rejects.toThrow( await expect(
'Max tokens must be between 1 and 100000' provider.generateCompletion('Test', { maxTokens: 0 })
); ).rejects.toThrow('Max tokens must be between 1 and 100000');
}); });
it('should validate topP range', async () => { it('should validate topP range', async () => {
await expect(provider.generateCompletion('Test', { topP: 1.5 })).rejects.toThrow( await expect(
'Top-p must be between 0 and 1' provider.generateCompletion('Test', { topP: 1.5 })
); ).rejects.toThrow('Top-p must be between 0 and 1');
}); });
}); });
@@ -125,7 +128,9 @@ describe('BaseProvider', () => {
const provider = new MockProvider({ apiKey: 'test-key' }); const provider = new MockProvider({ apiKey: 'test-key' });
// Access protected method through type assertion // Access protected method through type assertion
const calculateDelay = (provider as any).calculateBackoffDelay.bind(provider); const calculateDelay = (provider as any).calculateBackoffDelay.bind(
provider
);
const delay1 = calculateDelay(1); const delay1 = calculateDelay(1);
const delay2 = calculateDelay(2); const delay2 = calculateDelay(2);
@@ -164,7 +169,9 @@ describe('BaseProvider', () => {
it('should identify rate limit errors correctly', () => { it('should identify rate limit errors correctly', () => {
const provider = new MockProvider({ apiKey: 'test-key' }); const provider = new MockProvider({ apiKey: 'test-key' });
const isRateLimitError = (provider as any).isRateLimitError.bind(provider); const isRateLimitError = (provider as any).isRateLimitError.bind(
provider
);
expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true); expect(isRateLimitError(new Error('Rate limit exceeded'))).toBe(true);
expect(isRateLimitError(new Error('Too many requests'))).toBe(true); expect(isRateLimitError(new Error('Too many requests'))).toBe(true);

View File

@@ -16,7 +16,12 @@ import {
version version
} from '@tm/core'; } from '@tm/core';
import type { PlaceholderTask, TaskId, TaskPriority, TaskStatus } from '@tm/core'; import type {
PlaceholderTask,
TaskId,
TaskPriority,
TaskStatus
} from '@tm/core';
describe('tm-core smoke tests', () => { describe('tm-core smoke tests', () => {
describe('package metadata', () => { describe('package metadata', () => {
@@ -45,7 +50,6 @@ describe('tm-core smoke tests', () => {
}); });
}); });
describe('placeholder storage', () => { describe('placeholder storage', () => {
it('should perform basic storage operations', async () => { it('should perform basic storage operations', async () => {
const storage = new PlaceholderStorage(); const storage = new PlaceholderStorage();

View File

@@ -5,7 +5,12 @@ export default defineConfig({
test: { test: {
globals: true, globals: true,
environment: 'node', environment: 'node',
include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts', 'src/**/*.test.ts', 'src/**/*.spec.ts'], include: [
'tests/**/*.test.ts',
'tests/**/*.spec.ts',
'src/**/*.test.ts',
'src/**/*.spec.ts'
],
exclude: ['node_modules', 'dist', '.git', '.cache'], exclude: ['node_modules', 'dist', '.git', '.cache'],
coverage: { coverage: {
provider: 'v8', provider: 'v8',