diff --git a/packages/tm-core/biome.json b/packages/tm-core/biome.json index d3f0b866..3845b85f 100644 --- a/packages/tm-core/biome.json +++ b/packages/tm-core/biome.json @@ -8,7 +8,13 @@ }, "files": { "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": { "enabled": true, diff --git a/packages/tm-core/src/constants/index.ts b/packages/tm-core/src/constants/index.ts new file mode 100644 index 00000000..bc2cbb51 --- /dev/null +++ b/packages/tm-core/src/constants/index.ts @@ -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 = { + done: '✓', + 'in-progress': '►', + blocked: '⭕', + pending: '○', + deferred: '⏸', + cancelled: '✗', + review: '👁' +} as const; + +/** + * Status colors for display (using chalk color names) + */ +export const STATUS_COLORS: Record = { + pending: 'yellow', + 'in-progress': 'blue', + done: 'green', + deferred: 'gray', + cancelled: 'red', + blocked: 'magenta', + review: 'cyan' +} as const; diff --git a/packages/tm-core/src/entities/task.entity.ts b/packages/tm-core/src/entities/task.entity.ts index 833755ac..89fcfa41 100644 --- a/packages/tm-core/src/entities/task.entity.ts +++ b/packages/tm-core/src/entities/task.entity.ts @@ -3,7 +3,12 @@ */ 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 @@ -64,11 +69,17 @@ export class TaskEntity implements Task { } 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) { - 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)) { @@ -184,7 +195,10 @@ export class TaskEntity implements Task { */ updateStatus(newStatus: TaskStatus): void { 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 diff --git a/packages/tm-core/src/errors/task-master-error.ts b/packages/tm-core/src/errors/task-master-error.ts index a582ac16..f2dbb710 100644 --- a/packages/tm-core/src/errors/task-master-error.ts +++ b/packages/tm-core/src/errors/task-master-error.ts @@ -215,7 +215,14 @@ export class TaskMasterError extends Error { private containsSensitiveInfo(obj: any): boolean { 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(); return sensitiveKeys.some((key) => objString.includes(key)); @@ -297,7 +304,9 @@ export class TaskMasterError extends Error { /** * Create a new error with additional context */ - public withContext(additionalContext: Partial): TaskMasterError { + public withContext( + additionalContext: Partial + ): TaskMasterError { return new TaskMasterError( this.message, this.code, diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 79a6e4bf..2883131a 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -17,11 +17,19 @@ export type * from './types/index'; // Re-export interfaces (types only to avoid conflicts) export type * from './interfaces/index'; +// Re-export constants +export * from './constants/index'; + // Re-export providers export * from './providers/index'; // 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'; // Re-export parser diff --git a/packages/tm-core/src/interfaces/ai-provider.interface.ts b/packages/tm-core/src/interfaces/ai-provider.interface.ts index 9fb83fec..275b3dad 100644 --- a/packages/tm-core/src/interfaces/ai-provider.interface.ts +++ b/packages/tm-core/src/interfaces/ai-provider.interface.ts @@ -282,7 +282,10 @@ export abstract class BaseAIProvider implements IAIProvider { } // Abstract methods that must be implemented by concrete classes - abstract generateCompletion(prompt: string, options?: AIOptions): Promise; + abstract generateCompletion( + prompt: string, + options?: AIOptions + ): Promise; abstract generateStreamingCompletion( prompt: string, options?: AIOptions @@ -306,7 +309,9 @@ export abstract class BaseAIProvider implements IAIProvider { const modelExists = availableModels.some((m) => m.id === model); 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; @@ -342,7 +347,11 @@ export abstract class BaseAIProvider implements IAIProvider { * @param duration - Request duration in milliseconds * @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; this.usageStats.totalRequests++; @@ -361,15 +370,18 @@ export abstract class BaseAIProvider implements IAIProvider { } // Update average response time - const totalTime = this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1); - this.usageStats.averageResponseTime = (totalTime + duration) / this.usageStats.totalRequests; + const totalTime = + this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1); + this.usageStats.averageResponseTime = + (totalTime + duration) / this.usageStats.totalRequests; // Update success rate const successCount = Math.floor( this.usageStats.successRate * (this.usageStats.totalRequests - 1) ); 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(); } diff --git a/packages/tm-core/src/interfaces/storage.interface.ts b/packages/tm-core/src/interfaces/storage.interface.ts index af93b501..0084fd8f 100644 --- a/packages/tm-core/src/interfaces/storage.interface.ts +++ b/packages/tm-core/src/interfaces/storage.interface.ts @@ -40,7 +40,11 @@ export interface IStorage { * @param tag - Optional tag context for the task * @returns Promise that resolves when update is complete */ - updateTask(taskId: string, updates: Partial, tag?: string): Promise; + updateTask( + taskId: string, + updates: Partial, + tag?: string + ): Promise; /** * Delete a task by ID @@ -173,7 +177,11 @@ export abstract class BaseStorage implements IStorage { abstract loadTasks(tag?: string): Promise; abstract saveTasks(tasks: Task[], tag?: string): Promise; abstract appendTasks(tasks: Task[], tag?: string): Promise; - abstract updateTask(taskId: string, updates: Partial, tag?: string): Promise; + abstract updateTask( + taskId: string, + updates: Partial, + tag?: string + ): Promise; abstract deleteTask(taskId: string, tag?: string): Promise; abstract exists(tag?: string): Promise; abstract loadMetadata(tag?: string): Promise; diff --git a/packages/tm-core/src/parser/index.ts b/packages/tm-core/src/parser/index.ts index 3920cd40..043f3998 100644 --- a/packages/tm-core/src/parser/index.ts +++ b/packages/tm-core/src/parser/index.ts @@ -22,7 +22,9 @@ export interface TaskParser { export class PlaceholderParser implements TaskParser { async parse(content: string): Promise { // 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) => ({ id: `task-${index + 1}`, title: line.trim().replace(/^-\s*/, ''), diff --git a/packages/tm-core/src/providers/ai/base-provider.ts b/packages/tm-core/src/providers/ai/base-provider.ts index e8ba6239..a4b109e6 100644 --- a/packages/tm-core/src/providers/ai/base-provider.ts +++ b/packages/tm-core/src/providers/ai/base-provider.ts @@ -3,8 +3,15 @@ * Provides common functionality, error handling, and retry logic */ -import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js'; -import type { AIOptions, AIResponse, IAIProvider } from '../../interfaces/ai-provider.interface.js'; +import { + ERROR_CODES, + TaskMasterError +} from '../../errors/task-master-error.js'; +import type { + AIOptions, + AIResponse, + IAIProvider +} from '../../interfaces/ai-provider.interface.js'; // Constants for retry logic const DEFAULT_MAX_RETRIES = 3; @@ -67,7 +74,10 @@ export abstract class BaseProvider implements IAIProvider { constructor(config: BaseProviderConfig) { 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.model = config.model || this.getDefaultModel(); @@ -77,11 +87,17 @@ export abstract class BaseProvider implements IAIProvider { * Template method for generating completions * Handles validation, retries, and error handling */ - async generateCompletion(prompt: string, options?: AIOptions): Promise { + async generateCompletion( + prompt: string, + options?: AIOptions + ): Promise { // Validate input const validation = this.validateInput(prompt, options); 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 @@ -94,7 +110,10 @@ export abstract class BaseProvider implements IAIProvider { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { 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; return this.handleResponse(result, duration, prepared); @@ -117,7 +136,10 @@ export abstract class BaseProvider implements IAIProvider { /** * Validate input prompt and options */ - protected validateInput(prompt: string, options?: AIOptions): ValidationResult { + protected validateInput( + prompt: string, + options?: AIOptions + ): ValidationResult { // Validate prompt if (!prompt || typeof prompt !== '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 { if (options.temperature !== undefined) { - if (options.temperature < MIN_TEMPERATURE || options.temperature > MAX_TEMPERATURE) { + if ( + options.temperature < MIN_TEMPERATURE || + options.temperature > MAX_TEMPERATURE + ) { return { valid: false, 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 < MIN_MAX_TOKENS || options.maxTokens > MAX_MAX_TOKENS) { + if ( + options.maxTokens < MIN_MAX_TOKENS || + options.maxTokens > MAX_MAX_TOKENS + ) { return { valid: false, 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 */ - protected prepareRequest(prompt: string, options?: AIOptions): PreparedRequest { + protected prepareRequest( + prompt: string, + options?: AIOptions + ): PreparedRequest { const defaultOptions = this.getDefaultOptions(); const mergedOptions = { ...defaultOptions, ...options }; @@ -203,8 +234,10 @@ export abstract class BaseProvider implements IAIProvider { duration: number, request: PreparedRequest ): AIResponse { - const inputTokens = result.inputTokens || this.calculateTokens(request.prompt); - const outputTokens = result.outputTokens || this.calculateTokens(result.content); + const inputTokens = + result.inputTokens || this.calculateTokens(request.prompt); + const outputTokens = + result.outputTokens || this.calculateTokens(result.content); return { content: result.content, @@ -320,7 +353,8 @@ export abstract class BaseProvider implements IAIProvider { * Calculate exponential backoff delay with jitter */ 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); // Add jitter to prevent thundering herd @@ -394,11 +428,16 @@ export abstract class BaseProvider implements IAIProvider { options?: AIOptions ): AsyncIterator>; abstract isAvailable(): Promise; - abstract getProviderInfo(): import('../../interfaces/ai-provider.interface.js').ProviderInfo; - abstract getAvailableModels(): import('../../interfaces/ai-provider.interface.js').AIModel[]; + abstract getProviderInfo(): import( + '../../interfaces/ai-provider.interface.js' + ).ProviderInfo; + abstract getAvailableModels(): import( + '../../interfaces/ai-provider.interface.js' + ).AIModel[]; abstract validateCredentials(): Promise; abstract getUsageStats(): Promise< - import('../../interfaces/ai-provider.interface.js').ProviderUsageStats | null + | import('../../interfaces/ai-provider.interface.js').ProviderUsageStats + | null >; abstract initialize(): Promise; abstract close(): Promise; diff --git a/packages/tm-core/src/services/task-service.ts b/packages/tm-core/src/services/task-service.ts index 5b6cdcd5..df523859 100644 --- a/packages/tm-core/src/services/task-service.ts +++ b/packages/tm-core/src/services/task-service.ts @@ -49,7 +49,7 @@ export class TaskService { constructor(configManager: ConfigManager) { this.configManager = configManager; - + // Storage will be created during initialization this.storage = null as any; } @@ -66,7 +66,7 @@ export class TaskService { // Create storage based on configuration const storageConfig = this.configManager.getStorageConfig(); const projectRoot = this.configManager.getProjectRoot(); - + this.storage = StorageFactory.create( { storage: storageConfig } as any, projectRoot @@ -74,7 +74,7 @@ export class TaskService { // Initialize storage await this.storage.initialize(); - + this.initialized = true; } @@ -103,11 +103,11 @@ export class TaskService { } // Convert back to plain objects - let tasks = filteredEntities.map(entity => entity.toJSON()); + let tasks = filteredEntities.map((entity) => entity.toJSON()); // Handle subtasks option if (options.includeSubtasks === false) { - tasks = tasks.map(task => ({ + tasks = tasks.map((task) => ({ ...task, subtasks: [] })); @@ -124,7 +124,7 @@ export class TaskService { throw new TaskMasterError( 'Failed to get task list', ERROR_CODES.INTERNAL_ERROR, - { + { operation: 'getTaskList', tag, hasFilter: !!options.filter @@ -138,28 +138,28 @@ export class TaskService { * Get a single task by ID */ async getTask(taskId: string, tag?: string): Promise { - const result = await this.getTaskList({ + const result = await this.getTaskList({ 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 */ async getTasksByStatus( - status: TaskStatus | TaskStatus[], + status: TaskStatus | TaskStatus[], tag?: string ): Promise { const statuses = Array.isArray(status) ? status : [status]; - + const result = await this.getTaskList({ tag, filter: { status: statuses } }); - + return result.tasks; } @@ -173,9 +173,9 @@ export class TaskService { blocked: number; storageType: 'file' | 'api'; }> { - const result = await this.getTaskList({ + const result = await this.getTaskList({ tag, - includeSubtasks: true + includeSubtasks: true }); const stats = { @@ -188,22 +188,27 @@ export class TaskService { // Initialize all statuses const allStatuses: TaskStatus[] = [ - 'pending', 'in-progress', 'done', - 'deferred', 'cancelled', 'blocked', 'review' + 'pending', + 'in-progress', + 'done', + 'deferred', + 'cancelled', + 'blocked', + 'review' ]; - - allStatuses.forEach(status => { + + allStatuses.forEach((status) => { stats.byStatus[status] = 0; }); // Count tasks - result.tasks.forEach(task => { + result.tasks.forEach((task) => { stats.byStatus[task.status]++; - + if (task.subtasks && task.subtasks.length > 0) { stats.withSubtasks++; } - + if (task.status === 'blocked') { stats.blocked++; } @@ -225,21 +230,19 @@ export class TaskService { // Find tasks with no dependencies or all dependencies satisfied const completedIds = new Set( - result.tasks - .filter(t => t.status === 'done') - .map(t => t.id) + result.tasks.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') { return false; } - + if (!task.dependencies || task.dependencies.length === 0) { return true; } - - return task.dependencies.every(depId => + + return task.dependencies.every((depId) => completedIds.has(depId.toString()) ); }); @@ -259,10 +262,12 @@ export class TaskService { * Apply filters to task entities */ private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] { - return tasks.filter(task => { + return tasks.filter((task) => { // Status filter 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)) { return false; } @@ -270,7 +275,9 @@ export class TaskService { // Priority filter 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)) { return false; } @@ -278,7 +285,10 @@ export class TaskService { // Tags filter 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; } } @@ -292,8 +302,8 @@ export class TaskService { // Complexity filter if (filter.complexity) { - const complexities = Array.isArray(filter.complexity) - ? filter.complexity + const complexities = Array.isArray(filter.complexity) + ? filter.complexity : [filter.complexity]; if (!task.complexity || !complexities.includes(task.complexity)) { return false; @@ -304,9 +314,11 @@ export class TaskService { if (filter.search) { const searchLower = filter.search.toLowerCase(); 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); - + if (!inTitle && !inDescription && !inDetails) { return false; } @@ -353,4 +365,4 @@ export class TaskService { async setActiveTag(tag: string): Promise { await this.configManager.setActiveTag(tag); } -} \ No newline at end of file +} diff --git a/packages/tm-core/src/storage/api-storage.ts b/packages/tm-core/src/storage/api-storage.ts index cce01184..4e443979 100644 --- a/packages/tm-core/src/storage/api-storage.ts +++ b/packages/tm-core/src/storage/api-storage.ts @@ -3,7 +3,10 @@ * 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 { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; @@ -45,7 +48,7 @@ export class ApiStorage implements IStorage { constructor(config: ApiStorageConfig) { this.validateConfig(config); - + this.config = { endpoint: config.endpoint.replace(/\/$/, ''), // Remove trailing slash accessToken: config.accessToken, @@ -111,7 +114,7 @@ export class ApiStorage implements IStorage { */ private async verifyConnection(): Promise { const response = await this.makeRequest<{ status: string }>('/health'); - + if (!response.success) { throw new Error(`API health check failed: ${response.error}`); } @@ -124,7 +127,7 @@ export class ApiStorage implements IStorage { await this.ensureInitialized(); try { - const endpoint = tag + const endpoint = tag ? `/projects/${this.config.projectId}/tasks?tag=${encodeURIComponent(tag)}` : `/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`; - const response = await this.makeRequest<{ metadata: TaskMetadata }>(endpoint); + const response = await this.makeRequest<{ metadata: TaskMetadata }>( + endpoint + ); if (!response.success) { return null; @@ -355,10 +360,10 @@ export class ApiStorage implements IStorage { try { // First load existing tasks const existingTasks = await this.loadTasks(tag); - + // Append new tasks const allTasks = [...existingTasks, ...tasks]; - + // Save all tasks await this.saveTasks(allTasks, tag); } catch (error) { @@ -374,20 +379,24 @@ export class ApiStorage implements IStorage { /** * Update a specific task */ - async updateTask(taskId: string, updates: Partial, tag?: string): Promise { + async updateTask( + taskId: string, + updates: Partial, + tag?: string + ): Promise { await this.ensureInitialized(); try { // Load the task const task = await this.loadTask(taskId, tag); - + if (!task) { throw new Error(`Task ${taskId} not found`); } // Merge updates const updatedTask = { ...task, ...updates, id: taskId }; - + // Save updated task await this.saveTask(updatedTask, tag); } catch (error) { @@ -500,13 +509,15 @@ export class ApiStorage implements IStorage { } // Return stats or default values - return response.data?.stats || { - totalTasks: 0, - totalTags: 0, - storageSize: 0, - lastModified: new Date().toISOString(), - tagStats: [] - }; + return ( + response.data?.stats || { + totalTasks: 0, + totalTags: 0, + storageSize: 0, + lastModified: new Date().toISOString(), + tagStats: [] + } + ); } catch (error) { throw new TaskMasterError( 'Failed to get stats from API', @@ -627,9 +638,9 @@ export class ApiStorage implements IStorage { const options: RequestInit = { method, headers: { - 'Authorization': `Bearer ${this.config.accessToken}`, + Authorization: `Bearer ${this.config.accessToken}`, 'Content-Type': 'application/json', - 'Accept': 'application/json' + Accept: 'application/json' }, signal: controller.signal }; @@ -654,16 +665,16 @@ export class ApiStorage implements IStorage { // Handle specific error codes if (response.status === 401) { - return { - success: false, - error: 'Authentication failed - check access token' + return { + success: false, + error: 'Authentication failed - check access token' }; } if (response.status === 404) { - return { - success: false, - error: 'Resource not found' + return { + success: false, + error: 'Resource not found' }; } @@ -678,7 +689,10 @@ export class ApiStorage implements IStorage { const errorData = data as any; return { success: false, - error: errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}` + error: + errorData.error || + errorData.message || + `HTTP ${response.status}: ${response.statusText}` }; } catch (error) { lastError = error as Error; @@ -705,6 +719,6 @@ export class ApiStorage implements IStorage { * Delay helper for retries */ private delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } -} \ No newline at end of file +} diff --git a/packages/tm-core/src/storage/file-storage.ts b/packages/tm-core/src/storage/file-storage.ts index 5babd11d..ef70fb7c 100644 --- a/packages/tm-core/src/storage/file-storage.ts +++ b/packages/tm-core/src/storage/file-storage.ts @@ -5,7 +5,10 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; 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 @@ -80,7 +83,7 @@ export class FileStorage implements IStorage { totalTags: tags.length, lastModified: lastModified || new Date().toISOString(), storageSize: 0, // Could calculate actual file sizes if needed - tagStats: tags.map(tag => ({ + tagStats: tags.map((tag) => ({ tag, taskCount: 0, // Would need to load each tag to get accurate count lastModified: lastModified || new Date().toISOString() @@ -218,14 +221,18 @@ export class FileStorage implements IStorage { /** * Update a specific task */ - async updateTask(taskId: string, updates: Partial, tag?: string): Promise { + async updateTask( + taskId: string, + updates: Partial, + tag?: string + ): Promise { 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) { throw new Error(`Task ${taskId} not found`); } - + tasks[taskIndex] = { ...tasks[taskIndex], ...updates, id: taskId }; await this.saveTasks(tasks, tag); } @@ -235,12 +242,12 @@ export class FileStorage implements IStorage { */ async deleteTask(taskId: string, tag?: string): Promise { 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) { throw new Error(`Task ${taskId} not found`); } - + await this.saveTasks(filteredTasks, tag); } @@ -264,11 +271,13 @@ export class FileStorage implements IStorage { async renameTag(oldTag: string, newTag: string): Promise { const oldPath = this.getTasksPath(oldTag); const newPath = this.getTasksPath(newTag); - + try { await fs.rename(oldPath, newPath); } 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 { const tasks = await this.loadTasks(sourceTag); const metadata = await this.loadMetadata(sourceTag); - + await this.saveTasks(tasks, targetTag); if (metadata) { await this.saveMetadata(metadata, targetTag); @@ -323,7 +332,9 @@ export class FileStorage implements IStorage { /** * Read and parse JSON file with error handling */ - private async readJsonFile(filePath: string): Promise { + private async readJsonFile( + filePath: string + ): Promise { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); @@ -341,7 +352,10 @@ export class FileStorage implements IStorage { /** * Write JSON file with atomic operation using temp file */ - private async writeJsonFile(filePath: string, data: FileStorageData): Promise { + private async writeJsonFile( + filePath: string, + data: FileStorageData + ): Promise { // Use file locking to prevent concurrent writes const lockKey = filePath; const existingLock = this.fileLocks.get(lockKey); @@ -363,7 +377,10 @@ export class FileStorage implements IStorage { /** * Perform the actual write operation */ - private async performWrite(filePath: string, data: FileStorageData): Promise { + private async performWrite( + filePath: string, + data: FileStorageData + ): Promise { const tempPath = `${filePath}.tmp`; try { @@ -406,11 +423,11 @@ export class FileStorage implements IStorage { try { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = this.getBackupPath(filePath, timestamp); - + // Ensure backup directory exists const backupDir = path.dirname(backupPath); await fs.mkdir(backupDir, { recursive: true }); - + await fs.copyFile(filePath, backupPath); // Clean up old backups if needed @@ -432,7 +449,9 @@ export class FileStorage implements IStorage { try { const files = await fs.readdir(dir); const backupFiles = files - .filter((f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json')) + .filter( + (f) => f.startsWith(`${basename}.backup.`) && f.endsWith('.json') + ) .sort() .reverse(); diff --git a/packages/tm-core/src/storage/index.ts b/packages/tm-core/src/storage/index.ts index f5bba2f6..aa00dd1b 100644 --- a/packages/tm-core/src/storage/index.ts +++ b/packages/tm-core/src/storage/index.ts @@ -9,7 +9,10 @@ export { ApiStorage, type ApiStorageConfig } from './api-storage.js'; export { StorageFactory } from './storage-factory.js'; // 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 export interface StorageAdapter { diff --git a/packages/tm-core/src/storage/storage-factory.ts b/packages/tm-core/src/storage/storage-factory.ts index c0d60693..e652ea36 100644 --- a/packages/tm-core/src/storage/storage-factory.ts +++ b/packages/tm-core/src/storage/storage-factory.ts @@ -2,169 +2,169 @@ * @fileoverview Storage factory for creating appropriate storage implementations */ -import type { IStorage } from "../interfaces/storage.interface.js"; -import type { IConfiguration } from "../interfaces/configuration.interface.js"; -import { FileStorage } from "./file-storage.js"; -import { ApiStorage } from "./api-storage.js"; -import { ERROR_CODES, TaskMasterError } from "../errors/task-master-error.js"; +import type { IStorage } from '../interfaces/storage.interface.js'; +import type { IConfiguration } from '../interfaces/configuration.interface.js'; +import { FileStorage } from './file-storage.js'; +import { ApiStorage } from './api-storage.js'; +import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; /** * Factory for creating storage implementations based on configuration */ export class StorageFactory { - /** - * Create a storage implementation based on configuration - * @param config - Configuration object - * @param projectPath - Project root path (for file storage) - * @returns Storage implementation - */ - static create( - config: Partial, - projectPath: string - ): IStorage { - const storageType = config.storage?.type || "file"; + /** + * Create a storage implementation based on configuration + * @param config - Configuration object + * @param projectPath - Project root path (for file storage) + * @returns Storage implementation + */ + static create( + config: Partial, + projectPath: string + ): IStorage { + const storageType = config.storage?.type || 'file'; - switch (storageType) { - case "file": - return StorageFactory.createFileStorage(projectPath, config); + switch (storageType) { + case 'file': + return StorageFactory.createFileStorage(projectPath, config); - case "api": - return StorageFactory.createApiStorage(config); + case 'api': + return StorageFactory.createApiStorage(config); - default: - throw new TaskMasterError( - `Unknown storage type: ${storageType}`, - ERROR_CODES.INVALID_INPUT, - { storageType } - ); - } - } + default: + throw new TaskMasterError( + `Unknown storage type: ${storageType}`, + ERROR_CODES.INVALID_INPUT, + { storageType } + ); + } + } - /** - * Create file storage implementation - */ - private static createFileStorage( - projectPath: string, - config: Partial - ): FileStorage { - const basePath = config.storage?.basePath || projectPath; - return new FileStorage(basePath); - } + /** + * Create file storage implementation + */ + private static createFileStorage( + projectPath: string, + config: Partial + ): FileStorage { + const basePath = config.storage?.basePath || projectPath; + return new FileStorage(basePath); + } - /** - * Create API storage implementation - */ - private static createApiStorage(config: Partial): ApiStorage { - const { apiEndpoint, apiAccessToken } = config.storage || {}; + /** + * Create API storage implementation + */ + private static createApiStorage(config: Partial): ApiStorage { + const { apiEndpoint, apiAccessToken } = config.storage || {}; - if (!apiEndpoint) { - throw new TaskMasterError( - "API endpoint is required for API storage", - ERROR_CODES.MISSING_CONFIGURATION, - { storageType: "api" } - ); - } + if (!apiEndpoint) { + throw new TaskMasterError( + 'API endpoint is required for API storage', + ERROR_CODES.MISSING_CONFIGURATION, + { storageType: 'api' } + ); + } - if (!apiAccessToken) { - throw new TaskMasterError( - "API access token is required for API storage", - ERROR_CODES.MISSING_CONFIGURATION, - { storageType: "api" } - ); - } + if (!apiAccessToken) { + throw new TaskMasterError( + 'API access token is required for API storage', + ERROR_CODES.MISSING_CONFIGURATION, + { storageType: 'api' } + ); + } - return new ApiStorage({ - endpoint: apiEndpoint, - accessToken: apiAccessToken, - projectId: config.projectPath, - timeout: config.retry?.requestTimeout, - enableRetry: config.retry?.retryOnNetworkError, - maxRetries: config.retry?.retryAttempts, - }); - } + return new ApiStorage({ + endpoint: apiEndpoint, + accessToken: apiAccessToken, + projectId: config.projectPath, + timeout: config.retry?.requestTimeout, + enableRetry: config.retry?.retryOnNetworkError, + maxRetries: config.retry?.retryAttempts + }); + } - /** - * Detect optimal storage type based on available configuration - */ - static detectOptimalStorage(config: Partial): "file" | "api" { - // If API credentials are provided, prefer API storage (Hamster) - if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) { - return "api"; - } + /** + * Detect optimal storage type based on available configuration + */ + static detectOptimalStorage(config: Partial): 'file' | 'api' { + // If API credentials are provided, prefer API storage (Hamster) + if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) { + return 'api'; + } - // Default to file storage - return "file"; - } + // Default to file storage + return 'file'; + } - /** - * Validate storage configuration - */ - static validateStorageConfig(config: Partial): { - isValid: boolean; - errors: string[]; - } { - const errors: string[] = []; - const storageType = config.storage?.type; + /** + * Validate storage configuration + */ + static validateStorageConfig(config: Partial): { + isValid: boolean; + errors: string[]; + } { + const errors: string[] = []; + const storageType = config.storage?.type; - if (!storageType) { - errors.push("Storage type is not specified"); - return { isValid: false, errors }; - } + if (!storageType) { + errors.push('Storage type is not specified'); + return { isValid: false, errors }; + } - switch (storageType) { - case "api": - if (!config.storage?.apiEndpoint) { - errors.push("API endpoint is required for API storage"); - } - if (!config.storage?.apiAccessToken) { - errors.push("API access token is required for API storage"); - } - break; + switch (storageType) { + case 'api': + if (!config.storage?.apiEndpoint) { + errors.push('API endpoint is required for API storage'); + } + if (!config.storage?.apiAccessToken) { + errors.push('API access token is required for API storage'); + } + break; - case "file": - // File storage doesn't require additional config - break; + case 'file': + // File storage doesn't require additional config + break; - default: - errors.push(`Unknown storage type: ${storageType}`); - } + default: + errors.push(`Unknown storage type: ${storageType}`); + } - return { - isValid: errors.length === 0, - errors, - }; - } + return { + isValid: errors.length === 0, + errors + }; + } - /** - * Check if Hamster (API storage) is available - */ - static isHamsterAvailable(config: Partial): boolean { - return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken); - } + /** + * Check if Hamster (API storage) is available + */ + static isHamsterAvailable(config: Partial): boolean { + return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken); + } - /** - * Create a storage implementation with fallback - * Tries API storage first, falls back to file storage - */ - static async createWithFallback( - config: Partial, - projectPath: string - ): Promise { - // Try API storage if configured - if (StorageFactory.isHamsterAvailable(config)) { - try { - const apiStorage = StorageFactory.createApiStorage(config); - await apiStorage.initialize(); - return apiStorage; - } catch (error) { - console.warn( - "Failed to initialize API storage, falling back to file storage:", - error - ); - } - } + /** + * Create a storage implementation with fallback + * Tries API storage first, falls back to file storage + */ + static async createWithFallback( + config: Partial, + projectPath: string + ): Promise { + // Try API storage if configured + if (StorageFactory.isHamsterAvailable(config)) { + try { + const apiStorage = StorageFactory.createApiStorage(config); + await apiStorage.initialize(); + return apiStorage; + } catch (error) { + console.warn( + 'Failed to initialize API storage, falling back to file storage:', + error + ); + } + } - // Fallback to file storage - return StorageFactory.createFileStorage(projectPath, config); - } + // Fallback to file storage + return StorageFactory.createFileStorage(projectPath, config); + } } diff --git a/packages/tm-core/src/task-master-core.ts b/packages/tm-core/src/task-master-core.ts index 8d99f20d..2a07061d 100644 --- a/packages/tm-core/src/task-master-core.ts +++ b/packages/tm-core/src/task-master-core.ts @@ -3,7 +3,11 @@ */ 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 type { IConfiguration } from './interfaces/configuration.interface.js'; import type { Task, TaskStatus, TaskFilter } from './types/index.js'; @@ -33,7 +37,10 @@ export class TaskMasterCore { constructor(options: TaskMasterCoreOptions) { 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 @@ -108,7 +115,10 @@ export class TaskMasterCore { /** * Get tasks by status */ - async getTasksByStatus(status: TaskStatus | TaskStatus[], tag?: string): Promise { + async getTasksByStatus( + status: TaskStatus | TaskStatus[], + tag?: string + ): Promise { await this.ensureInitialized(); return this.taskService.getTasksByStatus(status, tag); } diff --git a/packages/tm-core/src/types/index.ts b/packages/tm-core/src/types/index.ts index 6299b18f..e877857a 100644 --- a/packages/tm-core/src/types/index.ts +++ b/packages/tm-core/src/types/index.ts @@ -103,7 +103,10 @@ export interface TaskCollection { /** * Type for creating a new task (without generated fields) */ -export type CreateTask = Omit & { +export type CreateTask = Omit< + Task, + 'id' | 'createdAt' | 'updatedAt' | 'subtasks' +> & { subtasks?: Omit[]; }; @@ -145,7 +148,15 @@ export interface TaskSortOptions { export function isTaskStatus(value: unknown): value is TaskStatus { return ( 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 */ 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 { return ( - typeof value === 'string' && ['simple', 'moderate', 'complex', 'very-complex'].includes(value) + typeof value === 'string' && + ['simple', 'moderate', 'complex', 'very-complex'].includes(value) ); } diff --git a/packages/tm-core/src/utils/id-generator.ts b/packages/tm-core/src/utils/id-generator.ts index a28d12a7..1ffe41d2 100644 --- a/packages/tm-core/src/utils/id-generator.ts +++ b/packages/tm-core/src/utils/id-generator.ts @@ -33,9 +33,14 @@ export function generateTaskId(): string { * // 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 - const parentSubtasks = existingSubtasks.filter((id) => id.startsWith(`${parentId}.`)); + const parentSubtasks = existingSubtasks.filter((id) => + id.startsWith(`${parentId}.`) + ); // Extract sequential numbers and find the highest const sequentialNumbers = parentSubtasks @@ -48,7 +53,8 @@ export function generateSubtaskId(parentId: string, existingSubtasks: string[] = .sort((a, b) => a - b); // 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}`; } diff --git a/packages/tm-core/tests/integration/list-tasks.test.ts b/packages/tm-core/tests/integration/list-tasks.test.ts index 6342db36..86623cc4 100644 --- a/packages/tm-core/tests/integration/list-tasks.test.ts +++ b/packages/tm-core/tests/integration/list-tasks.test.ts @@ -323,7 +323,9 @@ describe('TaskMasterCore - listTasks E2E', () => { it('should validate task entities', async () => { // 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'); 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); @@ -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( tagFile, JSON.stringify({ diff --git a/packages/tm-core/tests/mocks/mock-provider.ts b/packages/tm-core/tests/mocks/mock-provider.ts index a1a17002..eaa6e39d 100644 --- a/packages/tm-core/tests/mocks/mock-provider.ts +++ b/packages/tm-core/tests/mocks/mock-provider.ts @@ -58,7 +58,10 @@ export class MockProvider extends BaseProvider { 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) { throw new Error('Rate limit exceeded - too many requests (429)'); } @@ -200,6 +203,8 @@ export class MockProvider extends BaseProvider { // Override retry configuration for testing protected getMaxRetries(): number { - return this.options.failAfterAttempts ? this.options.failAfterAttempts + 1 : 3; + return this.options.failAfterAttempts + ? this.options.failAfterAttempts + 1 + : 3; } } diff --git a/packages/tm-core/tests/unit/base-provider.test.ts b/packages/tm-core/tests/unit/base-provider.test.ts index 14f2211c..6b11dbe5 100644 --- a/packages/tm-core/tests/unit/base-provider.test.ts +++ b/packages/tm-core/tests/unit/base-provider.test.ts @@ -3,7 +3,10 @@ */ 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'; describe('BaseProvider', () => { @@ -64,21 +67,21 @@ describe('BaseProvider', () => { }); it('should validate temperature range', async () => { - await expect(provider.generateCompletion('Test', { temperature: 3 })).rejects.toThrow( - 'Temperature must be between 0 and 2' - ); + await expect( + provider.generateCompletion('Test', { temperature: 3 }) + ).rejects.toThrow('Temperature must be between 0 and 2'); }); it('should validate maxTokens range', async () => { - await expect(provider.generateCompletion('Test', { maxTokens: 0 })).rejects.toThrow( - 'Max tokens must be between 1 and 100000' - ); + await expect( + provider.generateCompletion('Test', { maxTokens: 0 }) + ).rejects.toThrow('Max tokens must be between 1 and 100000'); }); it('should validate topP range', async () => { - await expect(provider.generateCompletion('Test', { topP: 1.5 })).rejects.toThrow( - 'Top-p must be between 0 and 1' - ); + await expect( + 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' }); // 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 delay2 = calculateDelay(2); @@ -164,7 +169,9 @@ describe('BaseProvider', () => { it('should identify rate limit errors correctly', () => { 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('Too many requests'))).toBe(true); diff --git a/packages/tm-core/tests/unit/smoke.test.ts b/packages/tm-core/tests/unit/smoke.test.ts index 3490cc2a..3257138b 100644 --- a/packages/tm-core/tests/unit/smoke.test.ts +++ b/packages/tm-core/tests/unit/smoke.test.ts @@ -16,7 +16,12 @@ import { version } 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('package metadata', () => { @@ -45,7 +50,6 @@ describe('tm-core smoke tests', () => { }); }); - describe('placeholder storage', () => { it('should perform basic storage operations', async () => { const storage = new PlaceholderStorage(); diff --git a/packages/tm-core/vitest.config.ts b/packages/tm-core/vitest.config.ts index 21c54df3..8b3e4e81 100644 --- a/packages/tm-core/vitest.config.ts +++ b/packages/tm-core/vitest.config.ts @@ -5,7 +5,12 @@ export default defineConfig({ test: { globals: true, 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'], coverage: { provider: 'v8',