diff --git a/.github/workflows/extension-ci.yml b/.github/workflows/extension-ci.yml index 8e59a341..91e05cc2 100644 --- a/.github/workflows/extension-ci.yml +++ b/.github/workflows/extension-ci.yml @@ -6,15 +6,15 @@ on: - main - next paths: - - 'apps/extension/**' - - '.github/workflows/extension-ci.yml' + - "apps/extension/**" + - ".github/workflows/extension-ci.yml" pull_request: branches: - main - next paths: - - 'apps/extension/**' - - '.github/workflows/extension-ci.yml' + - "apps/extension/**" + - ".github/workflows/extension-ci.yml" permissions: contents: read @@ -55,7 +55,6 @@ jobs: with: node-version: 20 - - name: Restore node_modules uses: actions/cache@v4 with: @@ -72,7 +71,7 @@ jobs: - name: Type Check Extension working-directory: apps/extension - run: npm run check-types + run: npm run typecheck env: FORCE_COLOR: 1 @@ -86,7 +85,6 @@ jobs: with: node-version: 20 - - name: Restore node_modules uses: actions/cache@v4 with: @@ -137,4 +135,3 @@ jobs: apps/extension/vsix-build/*.vsix apps/extension/dist/ retention-days: 30 - diff --git a/.github/workflows/extension-release.yml b/.github/workflows/extension-release.yml index be536522..223fb08d 100644 --- a/.github/workflows/extension-release.yml +++ b/.github/workflows/extension-release.yml @@ -37,7 +37,7 @@ jobs: - name: Type Check Extension working-directory: apps/extension - run: npm run check-types + run: npm run typecheck env: FORCE_COLOR: 1 @@ -107,4 +107,4 @@ jobs: echo "πŸŽ‰ Extension ${{ github.ref_name }} successfully published!" echo "πŸ“¦ Available on VS Code Marketplace" echo "🌍 Available on Open VSX Registry" - echo "🏷️ GitHub release created: ${{ github.ref_name }}" \ No newline at end of file + echo "🏷️ GitHub release created: ${{ github.ref_name }}" diff --git a/CLAUDE.md b/CLAUDE.md index 026d127f..a94c68cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,48 @@ }); ``` +## Architecture Guidelines + +### Business Logic Separation + +**CRITICAL RULE**: ALL business logic must live in `@tm/core`, NOT in presentation layers. + +- **`@tm/core`** (packages/tm-core/): + - Contains ALL business logic, domain models, services, and utilities + - Provides clean facade APIs through domain objects (tasks, auth, workflow, git, config) + - Houses all complexity - parsing, validation, transformations, calculations, etc. + - Example: Task ID parsing, subtask extraction, status validation, dependency resolution + +- **`@tm/cli`** (apps/cli/): + - Thin presentation layer ONLY + - Calls tm-core methods and displays results + - Handles CLI-specific concerns: argument parsing, output formatting, user prompts + - NO business logic, NO data transformations, NO calculations + +- **`@tm/mcp`** (apps/mcp/): + - Thin presentation layer ONLY + - Calls tm-core methods and returns MCP-formatted responses + - Handles MCP-specific concerns: tool schemas, parameter validation, response formatting + - NO business logic, NO data transformations, NO calculations + +- **`apps/extension`** (future): + - Thin presentation layer ONLY + - Calls tm-core methods and displays in VS Code UI + - NO business logic + +**Examples of violations to avoid:** + +- ❌ Creating helper functions in CLI/MCP to parse task IDs β†’ Move to tm-core +- ❌ Data transformation logic in CLI/MCP β†’ Move to tm-core +- ❌ Validation logic in CLI/MCP β†’ Move to tm-core +- ❌ Duplicating logic across CLI and MCP β†’ Implement once in tm-core + +**Correct approach:** +- βœ… Add method to TasksDomain: `tasks.get(taskId)` (automatically handles task and subtask IDs) +- βœ… CLI calls: `await tmCore.tasks.get(taskId)` (supports "1", "1.2", "HAM-123", "HAM-123.2") +- βœ… MCP calls: `await tmCore.tasks.get(taskId)` (same intelligent ID parsing) +- βœ… Single source of truth in tm-core + ## Documentation Guidelines - **Documentation location**: Write docs in `apps/docs/` (Mintlify site source), not `docs/` diff --git a/apps/cli/src/commands/auth.command.ts b/apps/cli/src/commands/auth.command.ts index 7c2e03c3..b6cd41f5 100644 --- a/apps/cli/src/commands/auth.command.ts +++ b/apps/cli/src/commands/auth.command.ts @@ -12,7 +12,7 @@ import { AuthManager, AuthenticationError, type AuthCredentials -} from '@tm/core/auth'; +} from '@tm/core'; import * as ui from '../utils/ui.js'; import { ContextCommand } from './context.command.js'; import { displayError } from '../utils/error-handler.js'; diff --git a/apps/cli/src/commands/autopilot.command.ts b/apps/cli/src/commands/autopilot.command.ts index 75608631..6e0b12e6 100644 --- a/apps/cli/src/commands/autopilot.command.ts +++ b/apps/cli/src/commands/autopilot.command.ts @@ -8,12 +8,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import boxen from 'boxen'; import ora, { type Ora } from 'ora'; -import { - createTaskMasterCore, - type TaskMasterCore, - type Task, - type Subtask -} from '@tm/core'; +import { createTmCore, type TmCore, type Task, type Subtask } from '@tm/core'; import * as ui from '../utils/ui.js'; /** @@ -60,7 +55,7 @@ export interface AutopilotCommandResult { * This is a thin presentation layer over @tm/core's autopilot functionality */ export class AutopilotCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: AutopilotCommandResult; constructor(name?: string) { @@ -164,11 +159,11 @@ export class AutopilotCommand extends Command { } /** - * Initialize TaskMasterCore + * Initialize TmCore */ private async initializeCore(projectRoot: string): Promise { if (!this.tmCore) { - this.tmCore = await createTaskMasterCore({ projectPath: projectRoot }); + this.tmCore = await createTmCore({ projectPath: projectRoot }); } } @@ -177,11 +172,11 @@ export class AutopilotCommand extends Command { */ private async loadTask(taskId: string): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } try { - const { task } = await this.tmCore.getTaskWithSubtask(taskId); + const { task } = await this.tmCore.tasks.get(taskId); return task; } catch (error) { return null; @@ -236,11 +231,7 @@ export class AutopilotCommand extends Command { } // Validate task structure and get execution order - const validationResult = await this.validateTaskStructure( - taskId, - task, - options - ); + const validationResult = await this.validateTaskStructure(taskId, task); if (!validationResult.success) { return validationResult; } @@ -288,19 +279,23 @@ export class AutopilotCommand extends Command { */ private async validateTaskStructure( taskId: string, - task: Task, - options: AutopilotCommandOptions + task: Task ): Promise { - const { TaskLoaderService } = await import('@tm/core'); + if (!this.tmCore) { + return { + success: false, + taskId, + task, + error: 'TmCore not initialized' + }; + } console.log(); console.log(chalk.cyan.bold('Validating task structure...')); - const taskLoader = new TaskLoaderService(options.project || process.cwd()); - const validationResult = await taskLoader.loadAndValidateTask(taskId); + const validationResult = await this.tmCore.tasks.loadAndValidate(taskId); if (!validationResult.success) { - await taskLoader.cleanup(); return { success: false, taskId, @@ -310,12 +305,10 @@ export class AutopilotCommand extends Command { }; } - const orderedSubtasks = taskLoader.getExecutionOrder( + const orderedSubtasks = this.tmCore.tasks.getExecutionOrder( validationResult.task! ); - await taskLoader.cleanup(); - return { success: true, taskId, @@ -499,7 +492,6 @@ export class AutopilotCommand extends Command { */ async cleanup(): Promise { if (this.tmCore) { - await this.tmCore.close(); this.tmCore = undefined; } } diff --git a/apps/cli/src/commands/autopilot/start.command.ts b/apps/cli/src/commands/autopilot/start.command.ts index 6bcae44c..17d6c3e4 100644 --- a/apps/cli/src/commands/autopilot/start.command.ts +++ b/apps/cli/src/commands/autopilot/start.command.ts @@ -3,7 +3,7 @@ */ import { Command } from 'commander'; -import { createTaskMasterCore, type WorkflowContext } from '@tm/core'; +import { createTmCore, type WorkflowContext } from '@tm/core'; import { AutopilotBaseOptions, hasWorkflowState, @@ -67,20 +67,19 @@ export class StartCommand extends Command { } // Initialize Task Master Core - const tmCore = await createTaskMasterCore({ + const tmCore = await createTmCore({ projectPath: mergedOptions.projectRoot! }); // Get current tag from ConfigManager - const currentTag = tmCore.getActiveTag(); + const currentTag = tmCore.config.getActiveTag(); // Load task formatter.info(`Loading task ${taskId}...`); - const { task } = await tmCore.getTaskWithSubtask(taskId); + const { task } = await tmCore.tasks.get(taskId); if (!task) { formatter.error('Task not found', { taskId }); - await tmCore.close(); process.exit(1); } @@ -90,7 +89,6 @@ export class StartCommand extends Command { taskId, suggestion: `Run: task-master expand --id=${taskId}` }); - await tmCore.close(); process.exit(1); } @@ -156,7 +154,6 @@ export class StartCommand extends Command { }); // Clean up - await tmCore.close(); } catch (error) { formatter.error((error as Error).message); if (mergedOptions.verbose) { diff --git a/apps/cli/src/commands/context.command.ts b/apps/cli/src/commands/context.command.ts index 0d933799..f032aeec 100644 --- a/apps/cli/src/commands/context.command.ts +++ b/apps/cli/src/commands/context.command.ts @@ -8,7 +8,7 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; import search from '@inquirer/search'; import ora, { Ora } from 'ora'; -import { AuthManager, type UserContext } from '@tm/core/auth'; +import { AuthManager, type UserContext } from '@tm/core'; import * as ui from '../utils/ui.js'; import { displayError } from '../utils/error-handler.js'; diff --git a/apps/cli/src/commands/export.command.ts b/apps/cli/src/commands/export.command.ts index 2b45a835..84cf2b3a 100644 --- a/apps/cli/src/commands/export.command.ts +++ b/apps/cli/src/commands/export.command.ts @@ -7,8 +7,13 @@ import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora, { Ora } from 'ora'; -import { AuthManager, type UserContext } from '@tm/core/auth'; -import { TaskMasterCore, type ExportResult } from '@tm/core'; +import { + AuthManager, + type UserContext, + type ExportResult, + createTmCore, + type TmCore +} from '@tm/core'; import * as ui from '../utils/ui.js'; import { displayError } from '../utils/error-handler.js'; @@ -28,7 +33,7 @@ export interface ExportCommandResult { */ export class ExportCommand extends Command { private authManager: AuthManager; - private taskMasterCore?: TaskMasterCore; + private taskMasterCore?: TmCore; private lastResult?: ExportCommandResult; constructor(name?: string) { @@ -61,7 +66,7 @@ export class ExportCommand extends Command { } /** - * Initialize the TaskMasterCore + * Initialize the TmCore */ private async initializeServices(): Promise { if (this.taskMasterCore) { @@ -69,8 +74,8 @@ export class ExportCommand extends Command { } try { - // Initialize TaskMasterCore - this.taskMasterCore = await TaskMasterCore.create({ + // Initialize TmCore + this.taskMasterCore = await createTmCore({ projectPath: process.cwd() }); } catch (error) { @@ -152,7 +157,8 @@ export class ExportCommand extends Command { // Perform export spinner = ora('Exporting tasks...').start(); - const exportResult = await this.taskMasterCore!.exportTasks({ + // Use integration domain facade + const exportResult = await this.taskMasterCore!.integration.exportTasks({ orgId, briefId, tag: options?.tag, diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index e6bafcc0..d1982e1e 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -6,16 +6,16 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { - createTaskMasterCore, + createTmCore, type Task, type TaskStatus, - type TaskMasterCore, + type TmCore, TASK_STATUSES, OUTPUT_FORMATS, STATUS_ICONS, type OutputFormat } from '@tm/core'; -import type { StorageType } from '@tm/core/types'; +import type { StorageType } from '@tm/core'; import * as ui from '../utils/ui.js'; import { displayError } from '../utils/error-handler.js'; import { displayCommandHeader } from '../utils/display-helpers.js'; @@ -59,7 +59,7 @@ export interface ListTasksResult { * This is a thin presentation layer over @tm/core */ export class ListTasksCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: ListTasksResult; constructor(name?: string) { @@ -144,11 +144,11 @@ export class ListTasksCommand extends Command { } /** - * Initialize TaskMasterCore + * Initialize TmCore */ private async initializeCore(projectRoot: string): Promise { if (!this.tmCore) { - this.tmCore = await createTaskMasterCore({ projectPath: projectRoot }); + this.tmCore = await createTmCore({ projectPath: projectRoot }); } } @@ -159,7 +159,7 @@ export class ListTasksCommand extends Command { options: ListCommandOptions ): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } // Build filter @@ -173,7 +173,7 @@ export class ListTasksCommand extends Command { : undefined; // Call tm-core - const result = await this.tmCore.getTaskList({ + const result = await this.tmCore.tasks.list({ tag: options.tag, filter, includeSubtasks: options.withSubtasks @@ -459,7 +459,6 @@ export class ListTasksCommand extends Command { */ async cleanup(): Promise { if (this.tmCore) { - await this.tmCore.close(); this.tmCore = undefined; } } diff --git a/apps/cli/src/commands/next.command.ts b/apps/cli/src/commands/next.command.ts index 50b37169..8f47b7ff 100644 --- a/apps/cli/src/commands/next.command.ts +++ b/apps/cli/src/commands/next.command.ts @@ -7,8 +7,8 @@ import path from 'node:path'; import { Command } from 'commander'; import chalk from 'chalk'; import boxen from 'boxen'; -import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; -import type { StorageType } from '@tm/core/types'; +import { createTmCore, type Task, type TmCore } from '@tm/core'; +import type { StorageType } from '@tm/core'; import { displayError } from '../utils/error-handler.js'; import { displayTaskDetails } from '../ui/components/task-detail.component.js'; import { displayCommandHeader } from '../utils/display-helpers.js'; @@ -38,7 +38,7 @@ export interface NextTaskResult { * This is a thin presentation layer over @tm/core */ export class NextCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: NextTaskResult; constructor(name?: string) { @@ -104,12 +104,12 @@ export class NextCommand extends Command { } /** - * Initialize TaskMasterCore + * Initialize TmCore */ private async initializeCore(projectRoot: string): Promise { if (!this.tmCore) { const resolved = path.resolve(projectRoot); - this.tmCore = await createTaskMasterCore({ projectPath: resolved }); + this.tmCore = await createTmCore({ projectPath: resolved }); } } @@ -120,18 +120,18 @@ export class NextCommand extends Command { options: NextCommandOptions ): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } // Call tm-core to get next task - const task = await this.tmCore.getNextTask(options.tag); + const task = await this.tmCore.tasks.getNext(options.tag); // Get storage type and active tag - const storageType = this.tmCore.getStorageType(); + const storageType = this.tmCore.config.getStorageConfig().type; if (storageType === 'auto') { throw new Error('Storage type must be resolved before use'); } - const activeTag = options.tag || this.tmCore.getActiveTag(); + const activeTag = options.tag || this.tmCore.config.getActiveTag(); return { task, @@ -232,7 +232,6 @@ export class NextCommand extends Command { */ async cleanup(): Promise { if (this.tmCore) { - await this.tmCore.close(); this.tmCore = undefined; } } diff --git a/apps/cli/src/commands/set-status.command.ts b/apps/cli/src/commands/set-status.command.ts index 9e08b1cd..15dcfd58 100644 --- a/apps/cli/src/commands/set-status.command.ts +++ b/apps/cli/src/commands/set-status.command.ts @@ -6,12 +6,8 @@ import { Command } from 'commander'; import chalk from 'chalk'; import boxen from 'boxen'; -import { - createTaskMasterCore, - type TaskMasterCore, - type TaskStatus -} from '@tm/core'; -import type { StorageType } from '@tm/core/types'; +import { createTmCore, type TmCore, type TaskStatus } from '@tm/core'; +import type { StorageType } from '@tm/core'; import { displayError } from '../utils/error-handler.js'; /** @@ -56,7 +52,7 @@ export interface SetStatusResult { * This is a thin presentation layer over @tm/core */ export class SetStatusCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: SetStatusResult; constructor(name?: string) { @@ -112,7 +108,7 @@ export class SetStatusCommand extends Command { } // Initialize TaskMaster core - this.tmCore = await createTaskMasterCore({ + this.tmCore = await createTmCore({ projectPath: options.project || process.cwd() }); @@ -128,7 +124,7 @@ export class SetStatusCommand extends Command { for (const taskId of taskIds) { try { - const result = await this.tmCore.updateTaskStatus( + const result = await this.tmCore.tasks.updateStatus( taskId, options.status ); @@ -168,7 +164,7 @@ export class SetStatusCommand extends Command { this.lastResult = { success: true, updatedTasks, - storageType: this.tmCore.getStorageType() as Exclude< + storageType: this.tmCore.config.getStorageConfig().type as Exclude< StorageType, 'auto' > @@ -188,7 +184,6 @@ export class SetStatusCommand extends Command { } finally { // Clean up resources if (this.tmCore) { - await this.tmCore.close(); } } diff --git a/apps/cli/src/commands/show.command.ts b/apps/cli/src/commands/show.command.ts index 121c0819..dd469563 100644 --- a/apps/cli/src/commands/show.command.ts +++ b/apps/cli/src/commands/show.command.ts @@ -6,8 +6,8 @@ import { Command } from 'commander'; import chalk from 'chalk'; import boxen from 'boxen'; -import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; -import type { StorageType } from '@tm/core/types'; +import { createTmCore, type Task, type TmCore } from '@tm/core'; +import type { StorageType } from '@tm/core'; import * as ui from '../utils/ui.js'; import { displayError } from '../utils/error-handler.js'; import { displayTaskDetails } from '../ui/components/task-detail.component.js'; @@ -47,7 +47,7 @@ export interface ShowMultipleTasksResult { * This is a thin presentation layer over @tm/core */ export class ShowCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: ShowTaskResult | ShowMultipleTasksResult; constructor(name?: string) { @@ -133,11 +133,11 @@ export class ShowCommand extends Command { } /** - * Initialize TaskMasterCore + * Initialize TmCore */ private async initializeCore(projectRoot: string): Promise { if (!this.tmCore) { - this.tmCore = await createTaskMasterCore({ projectPath: projectRoot }); + this.tmCore = await createTmCore({ projectPath: projectRoot }); } } @@ -149,18 +149,18 @@ export class ShowCommand extends Command { _options: ShowCommandOptions ): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } // Get the task - const task = await this.tmCore.getTask(taskId); + const result = await this.tmCore.tasks.get(taskId); // Get storage type - const storageType = this.tmCore.getStorageType(); + const storageType = this.tmCore.config.getStorageConfig().type; return { - task, - found: task !== null, + task: result.task, + found: result.task !== null, storageType: storageType as Exclude }; } @@ -173,7 +173,7 @@ export class ShowCommand extends Command { _options: ShowCommandOptions ): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } const tasks: Task[] = []; @@ -181,16 +181,16 @@ export class ShowCommand extends Command { // Get each task individually for (const taskId of taskIds) { - const task = await this.tmCore.getTask(taskId); - if (task) { - tasks.push(task); + const result = await this.tmCore.tasks.get(taskId); + if (result.task) { + tasks.push(result.task); } else { notFound.push(taskId); } } // Get storage type - const storageType = this.tmCore.getStorageType(); + const storageType = this.tmCore.config.getStorageConfig().type; return { tasks, @@ -253,7 +253,7 @@ export class ShowCommand extends Command { } // Display header with storage info - const activeTag = this.tmCore?.getActiveTag() || 'master'; + const activeTag = this.tmCore?.config.getActiveTag() || 'master'; displayCommandHeader(this.tmCore, { tag: activeTag, storageType: result.storageType @@ -276,7 +276,7 @@ export class ShowCommand extends Command { _options: ShowCommandOptions ): void { // Display header with storage info - const activeTag = this.tmCore?.getActiveTag() || 'master'; + const activeTag = this.tmCore?.config.getActiveTag() || 'master'; displayCommandHeader(this.tmCore, { tag: activeTag, storageType: result.storageType @@ -322,7 +322,6 @@ export class ShowCommand extends Command { */ async cleanup(): Promise { if (this.tmCore) { - await this.tmCore.close(); this.tmCore = undefined; } } diff --git a/apps/cli/src/commands/start.command.ts b/apps/cli/src/commands/start.command.ts index dbeb17a7..7558772b 100644 --- a/apps/cli/src/commands/start.command.ts +++ b/apps/cli/src/commands/start.command.ts @@ -10,8 +10,8 @@ import boxen from 'boxen'; import ora, { type Ora } from 'ora'; import { spawn } from 'child_process'; import { - createTaskMasterCore, - type TaskMasterCore, + createTmCore, + type TmCore, type StartTaskResult as CoreStartTaskResult } from '@tm/core'; import { displayTaskDetails } from '../ui/components/task-detail.component.js'; @@ -43,7 +43,7 @@ export interface StartCommandResult extends CoreStartTaskResult { * This is a thin presentation layer over @tm/core's TaskExecutionService */ export class StartCommand extends Command { - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; private lastResult?: StartCommandResult; constructor(name?: string) { @@ -147,7 +147,7 @@ export class StartCommand extends Command { // Convert core result to CLI result with storage type const result: StartCommandResult = { ...coreResult, - storageType: this.tmCore?.getStorageType() + storageType: this.tmCore?.config.getStorageConfig().type }; // Store result for programmatic access @@ -180,11 +180,11 @@ export class StartCommand extends Command { } /** - * Initialize TaskMasterCore + * Initialize TmCore */ private async initializeCore(projectRoot: string): Promise { if (!this.tmCore) { - this.tmCore = await createTaskMasterCore({ projectPath: projectRoot }); + this.tmCore = await createTmCore({ projectPath: projectRoot }); } } @@ -193,9 +193,9 @@ export class StartCommand extends Command { */ private async performGetNextTask(): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } - return this.tmCore.getNextAvailableTask(); + return this.tmCore.tasks.getNextAvailable(); } /** @@ -204,11 +204,10 @@ export class StartCommand extends Command { private async showPreLaunchMessage(targetTaskId: string): Promise { if (!this.tmCore) return; - const { task, subtask, subtaskId } = - await this.tmCore.getTaskWithSubtask(targetTaskId); + const { task, isSubtask } = await this.tmCore.tasks.get(targetTaskId); if (task) { - const workItemText = subtask - ? `Subtask #${task.id}.${subtaskId} - ${subtask.title}` + const workItemText = isSubtask + ? `Subtask #${targetTaskId} - ${task.title}` : `Task #${task.id} - ${task.title}`; console.log( @@ -227,7 +226,7 @@ export class StartCommand extends Command { options: StartCommandOptions ): Promise { if (!this.tmCore) { - throw new Error('TaskMasterCore not initialized'); + throw new Error('TmCore not initialized'); } // Show spinner for status update if enabled @@ -237,7 +236,7 @@ export class StartCommand extends Command { } // Get execution command from tm-core (instead of executing directly) - const result = await this.tmCore.startTask(targetTaskId, { + const result = await this.tmCore.tasks.start(targetTaskId, { dryRun: options.dryRun, force: options.force, updateStatus: !options.noStatusUpdate @@ -471,7 +470,6 @@ export class StartCommand extends Command { */ async cleanup(): Promise { if (this.tmCore) { - await this.tmCore.close(); this.tmCore = undefined; } } diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 55dd4dc9..17178b79 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -41,5 +41,5 @@ export type { Task, TaskStatus, TaskPriority, - TaskMasterCore + TmCore } from '@tm/core'; diff --git a/apps/cli/src/ui/components/dashboard.component.ts b/apps/cli/src/ui/components/dashboard.component.ts index 2f21d0c2..e8c883d6 100644 --- a/apps/cli/src/ui/components/dashboard.component.ts +++ b/apps/cli/src/ui/components/dashboard.component.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; -import type { Task, TaskPriority } from '@tm/core/types'; +import type { Task, TaskPriority } from '@tm/core'; import { getComplexityWithColor } from '../../utils/ui.js'; /** diff --git a/apps/cli/src/ui/components/next-task.component.ts b/apps/cli/src/ui/components/next-task.component.ts index 9de81c28..528ce353 100644 --- a/apps/cli/src/ui/components/next-task.component.ts +++ b/apps/cli/src/ui/components/next-task.component.ts @@ -5,7 +5,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; -import type { Task } from '@tm/core/types'; +import type { Task } from '@tm/core'; import { getComplexityWithColor, getBoxWidth } from '../../utils/ui.js'; /** diff --git a/apps/cli/src/ui/components/task-detail.component.ts b/apps/cli/src/ui/components/task-detail.component.ts index 2f874d2b..1b7e93b4 100644 --- a/apps/cli/src/ui/components/task-detail.component.ts +++ b/apps/cli/src/ui/components/task-detail.component.ts @@ -8,7 +8,7 @@ import boxen from 'boxen'; import Table from 'cli-table3'; import { marked, MarkedExtension } from 'marked'; import { markedTerminal } from 'marked-terminal'; -import type { Task } from '@tm/core/types'; +import type { Task } from '@tm/core'; import { getStatusWithColor, getPriorityWithColor, diff --git a/apps/cli/src/utils/display-helpers.ts b/apps/cli/src/utils/display-helpers.ts index acbce710..f7ebf8e0 100644 --- a/apps/cli/src/utils/display-helpers.ts +++ b/apps/cli/src/utils/display-helpers.ts @@ -3,73 +3,41 @@ * Provides DRY utilities for displaying headers and other command output */ -import type { TaskMasterCore } from '@tm/core'; -import type { StorageType } from '@tm/core/types'; -import { displayHeader, type BriefInfo } from '../ui/index.js'; - -/** - * Get web app base URL from environment - */ -function getWebAppUrl(): string | undefined { - const baseDomain = - process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN; - - if (!baseDomain) { - return undefined; - } - - // If it already includes protocol, use as-is - if (baseDomain.startsWith('http://') || baseDomain.startsWith('https://')) { - return baseDomain; - } - - // Otherwise, add protocol based on domain - if (baseDomain.includes('localhost') || baseDomain.includes('127.0.0.1')) { - return `http://${baseDomain}`; - } - - return `https://${baseDomain}`; -} +import type { TmCore } from '@tm/core'; +import type { StorageType } from '@tm/core'; +import { displayHeader } from '../ui/index.js'; /** * Display the command header with appropriate storage information * Handles both API and file storage displays */ export function displayCommandHeader( - tmCore: TaskMasterCore | undefined, + tmCore: TmCore | undefined, options: { tag?: string; storageType: Exclude; } ): void { - const { tag, storageType } = options; - - // Get brief info if using API storage - let briefInfo: BriefInfo | undefined; - if (storageType === 'api' && tmCore) { - const storageInfo = tmCore.getStorageDisplayInfo(); - if (storageInfo) { - // Construct full brief info with web app URL - briefInfo = { - ...storageInfo, - webAppUrl: getWebAppUrl() - }; - } + if (!tmCore) { + // Fallback display if tmCore is not available + displayHeader({ + tag: options.tag || 'master', + storageType: options.storageType + }); + return; } - // Get file path for display (only for file storage) - // Note: The file structure is fixed for file storage and won't change. - // This is a display-only relative path, not used for actual file operations. - const filePath = - storageType === 'file' && tmCore - ? `.taskmaster/tasks/tasks.json` - : undefined; + // Get the resolved storage type from tasks domain + const resolvedStorageType = tmCore.tasks.getStorageType(); - // Display header + // Get storage display info from tm-core (single source of truth) + const displayInfo = tmCore.auth.getStorageDisplayInfo(resolvedStorageType); + + // Display header with computed display info displayHeader({ - tag: tag || 'master', - filePath: filePath, - storageType: storageType === 'api' ? 'api' : 'file', - briefInfo: briefInfo + tag: options.tag || 'master', + filePath: displayInfo.filePath, + storageType: displayInfo.storageType, + briefInfo: displayInfo.briefInfo }); } diff --git a/apps/cli/src/utils/ui.ts b/apps/cli/src/utils/ui.ts index 048cf343..137b353f 100644 --- a/apps/cli/src/utils/ui.ts +++ b/apps/cli/src/utils/ui.ts @@ -6,7 +6,7 @@ import chalk from 'chalk'; import boxen from 'boxen'; import Table from 'cli-table3'; -import type { Task, TaskStatus, TaskPriority } from '@tm/core/types'; +import type { Task, TaskStatus, TaskPriority } from '@tm/core'; /** * Get colored status display with ASCII icons (matches scripts/modules/ui.js style) diff --git a/apps/extension/docs/extension-CI-setup.md b/apps/extension/docs/extension-CI-setup.md index 8616bd0b..e9b76a01 100644 --- a/apps/extension/docs/extension-CI-setup.md +++ b/apps/extension/docs/extension-CI-setup.md @@ -181,7 +181,7 @@ Workflows upload artifacts that you can download: - Check extension code compiles locally: `cd apps/extension && npm run build` - Verify tests pass locally: `npm run test` -- Check for TypeScript errors: `npm run check-types` +- Check for TypeScript errors: `npm run typecheck` #### Packaging Failures diff --git a/apps/extension/docs/extension-development-guide.md b/apps/extension/docs/extension-development-guide.md index df09b5a2..2e3fcf5d 100644 --- a/apps/extension/docs/extension-development-guide.md +++ b/apps/extension/docs/extension-development-guide.md @@ -61,7 +61,7 @@ npm run build:css npm run build # Type checking -npm run check-types +npm run typecheck # Linting npm run lint diff --git a/apps/extension/package.json b/apps/extension/package.json index dcb85d8a..e9cfb78c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -237,7 +237,7 @@ "watch": "npm run watch:js & npm run watch:css", "watch:js": "node ./esbuild.js --watch", "watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch", - "check-types": "tsc --noEmit" + "typecheck": "tsc --noEmit" }, "devDependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/apps/extension/src/services/terminal-manager.ts b/apps/extension/src/services/terminal-manager.ts index c4dab947..1fba630e 100644 --- a/apps/extension/src/services/terminal-manager.ts +++ b/apps/extension/src/services/terminal-manager.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode'; -import { createTaskMasterCore, type TaskMasterCore } from '@tm/core'; +import { createTmCore, type TmCore } from '@tm/core'; import type { ExtensionLogger } from '../utils/logger'; export interface TerminalExecutionOptions { @@ -21,7 +21,7 @@ export interface TerminalExecutionResult { export class TerminalManager { private terminals = new Map(); - private tmCore?: TaskMasterCore; + private tmCore?: TmCore; constructor( private context: vscode.ExtensionContext, @@ -49,7 +49,7 @@ export class TerminalManager { await this.initializeCore(); // Use tm-core to start the task (same as CLI) - const startResult = await this.tmCore!.startTask(taskId, { + const startResult = await this.tmCore!.tasks.start(taskId, { dryRun: false, force: false, updateStatus: true @@ -110,7 +110,7 @@ export class TerminalManager { if (!workspaceRoot) { throw new Error('No workspace folder found'); } - this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot }); + this.tmCore = await createTmCore({ projectPath: workspaceRoot }); } } @@ -144,13 +144,9 @@ export class TerminalManager { }); this.terminals.clear(); + // Clear tm-core reference (no explicit cleanup needed) if (this.tmCore) { - try { - await this.tmCore.close(); - this.tmCore = undefined; - } catch (error) { - this.logger.error('Failed to close tm-core:', error); - } + this.tmCore = undefined; } } } diff --git a/apps/mcp/src/tools/autopilot/start.tool.ts b/apps/mcp/src/tools/autopilot/start.tool.ts index 225a9670..e0dabdda 100644 --- a/apps/mcp/src/tools/autopilot/start.tool.ts +++ b/apps/mcp/src/tools/autopilot/start.tool.ts @@ -11,7 +11,7 @@ import { withNormalizedProjectRoot } from '../../shared/utils.js'; import type { MCPContext } from '../../shared/types.js'; -import { createTaskMasterCore } from '@tm/core'; +import { createTmCore } from '@tm/core'; import { WorkflowService } from '@tm/core'; import type { FastMCP } from 'fastmcp'; @@ -83,17 +83,16 @@ export function registerAutopilotStartTool(server: FastMCP) { } // Load task data and get current tag - const core = await createTaskMasterCore({ + const core = await createTmCore({ projectPath: projectRoot }); // Get current tag from ConfigManager - const currentTag = core.getActiveTag(); + const currentTag = core.config.getActiveTag(); - const taskResult = await core.getTaskWithSubtask(taskId); + const taskResult = await core.tasks.get(taskId); if (!taskResult || !taskResult.task) { - await core.close(); return handleApiResult({ result: { success: false, @@ -108,7 +107,6 @@ export function registerAutopilotStartTool(server: FastMCP) { // Validate task has subtasks if (!task.subtasks || task.subtasks.length === 0) { - await core.close(); return handleApiResult({ result: { success: false, diff --git a/package-lock.json b/package-lock.json index 9fcec1d2..0c80b99e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "@manypkg/cli": "^0.25.1", "@tm/ai-sdk-provider-grok-cli": "*", "@tm/cli": "*", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/marked-terminal": "^6.1.1", "concurrently": "^9.2.1", diff --git a/package.json b/package.json index 827e443d..314b6abb 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "@manypkg/cli": "^0.25.1", "@tm/ai-sdk-provider-grok-cli": "*", "@tm/cli": "*", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/marked-terminal": "^6.1.1", "concurrently": "^9.2.1", diff --git a/packages/tm-core/src/auth/index.ts b/packages/tm-core/src/auth/index.ts deleted file mode 100644 index 7700dc7a..00000000 --- a/packages/tm-core/src/auth/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Authentication module exports - */ - -export { AuthManager } from './auth-manager.js'; -export { CredentialStore } from './credential-store.js'; -export { OAuthService } from './oauth-service.js'; -export { SupabaseSessionStorage } from './supabase-session-storage.js'; -export type { - Organization, - Brief, - RemoteTask -} from '../services/organization.service.js'; - -export type { - AuthCredentials, - OAuthFlowOptions, - AuthConfig, - CliData, - UserContext -} from './types.js'; - -export { AuthenticationError } from './types.js'; - -export { - DEFAULT_AUTH_CONFIG, - getAuthConfig -} from './config.js'; diff --git a/packages/tm-core/src/constants/index.ts b/packages/tm-core/src/common/constants/index.ts similarity index 100% rename from packages/tm-core/src/constants/index.ts rename to packages/tm-core/src/common/constants/index.ts diff --git a/packages/tm-core/src/errors/index.ts b/packages/tm-core/src/common/errors/index.ts similarity index 100% rename from packages/tm-core/src/errors/index.ts rename to packages/tm-core/src/common/errors/index.ts diff --git a/packages/tm-core/src/errors/task-master-error.ts b/packages/tm-core/src/common/errors/task-master-error.ts similarity index 100% rename from packages/tm-core/src/errors/task-master-error.ts rename to packages/tm-core/src/common/errors/task-master-error.ts diff --git a/packages/tm-core/src/interfaces/configuration.interface.ts b/packages/tm-core/src/common/interfaces/configuration.interface.ts similarity index 100% rename from packages/tm-core/src/interfaces/configuration.interface.ts rename to packages/tm-core/src/common/interfaces/configuration.interface.ts diff --git a/packages/tm-core/src/interfaces/index.ts b/packages/tm-core/src/common/interfaces/index.ts similarity index 74% rename from packages/tm-core/src/interfaces/index.ts rename to packages/tm-core/src/common/interfaces/index.ts index 44b6876c..981f32d4 100644 --- a/packages/tm-core/src/interfaces/index.ts +++ b/packages/tm-core/src/common/interfaces/index.ts @@ -8,8 +8,8 @@ export type * from './storage.interface.js'; export * from './storage.interface.js'; // AI Provider interfaces -export type * from './ai-provider.interface.js'; -export * from './ai-provider.interface.js'; +export type * from '../../modules/ai/interfaces/ai-provider.interface.js'; +export * from '../../modules/ai/interfaces/ai-provider.interface.js'; // Configuration interfaces export type * from './configuration.interface.js'; diff --git a/packages/tm-core/src/interfaces/storage.interface.ts b/packages/tm-core/src/common/interfaces/storage.interface.ts similarity index 97% rename from packages/tm-core/src/interfaces/storage.interface.ts rename to packages/tm-core/src/common/interfaces/storage.interface.ts index 14f0a148..7ddd2bec 100644 --- a/packages/tm-core/src/interfaces/storage.interface.ts +++ b/packages/tm-core/src/common/interfaces/storage.interface.ts @@ -164,6 +164,12 @@ export interface IStorage { * @returns Promise that resolves to storage statistics */ getStats(): Promise; + + /** + * Get the storage type identifier + * @returns The type of storage implementation ('file' or 'api') + */ + getStorageType(): 'file' | 'api'; } /** @@ -241,6 +247,7 @@ export abstract class BaseStorage implements IStorage { abstract initialize(): Promise; abstract close(): Promise; abstract getStats(): Promise; + abstract getStorageType(): 'file' | 'api'; /** * Utility method to generate backup filename diff --git a/packages/tm-core/src/logger/factory.ts b/packages/tm-core/src/common/logger/factory.ts similarity index 100% rename from packages/tm-core/src/logger/factory.ts rename to packages/tm-core/src/common/logger/factory.ts diff --git a/packages/tm-core/src/logger/index.ts b/packages/tm-core/src/common/logger/index.ts similarity index 100% rename from packages/tm-core/src/logger/index.ts rename to packages/tm-core/src/common/logger/index.ts diff --git a/packages/tm-core/src/logger/logger.ts b/packages/tm-core/src/common/logger/logger.ts similarity index 100% rename from packages/tm-core/src/logger/logger.ts rename to packages/tm-core/src/common/logger/logger.ts diff --git a/packages/tm-core/src/mappers/TaskMapper.test.ts b/packages/tm-core/src/common/mappers/TaskMapper.test.ts similarity index 100% rename from packages/tm-core/src/mappers/TaskMapper.test.ts rename to packages/tm-core/src/common/mappers/TaskMapper.test.ts diff --git a/packages/tm-core/src/mappers/TaskMapper.ts b/packages/tm-core/src/common/mappers/TaskMapper.ts similarity index 100% rename from packages/tm-core/src/mappers/TaskMapper.ts rename to packages/tm-core/src/common/mappers/TaskMapper.ts diff --git a/packages/tm-core/src/types/database.types.ts b/packages/tm-core/src/common/types/database.types.ts similarity index 100% rename from packages/tm-core/src/types/database.types.ts rename to packages/tm-core/src/common/types/database.types.ts diff --git a/packages/tm-core/src/types/index.ts b/packages/tm-core/src/common/types/index.ts similarity index 100% rename from packages/tm-core/src/types/index.ts rename to packages/tm-core/src/common/types/index.ts diff --git a/packages/tm-core/src/types/legacy.ts b/packages/tm-core/src/common/types/legacy.ts similarity index 100% rename from packages/tm-core/src/types/legacy.ts rename to packages/tm-core/src/common/types/legacy.ts diff --git a/packages/tm-core/src/types/repository-types.ts b/packages/tm-core/src/common/types/repository-types.ts similarity index 100% rename from packages/tm-core/src/types/repository-types.ts rename to packages/tm-core/src/common/types/repository-types.ts diff --git a/packages/tm-core/src/utils/git-utils.ts b/packages/tm-core/src/common/utils/git-utils.ts similarity index 100% rename from packages/tm-core/src/utils/git-utils.ts rename to packages/tm-core/src/common/utils/git-utils.ts diff --git a/packages/tm-core/src/utils/id-generator.ts b/packages/tm-core/src/common/utils/id-generator.ts similarity index 100% rename from packages/tm-core/src/utils/id-generator.ts rename to packages/tm-core/src/common/utils/id-generator.ts diff --git a/packages/tm-core/src/utils/index.ts b/packages/tm-core/src/common/utils/index.ts similarity index 100% rename from packages/tm-core/src/utils/index.ts rename to packages/tm-core/src/common/utils/index.ts diff --git a/packages/tm-core/src/utils/path-normalizer.spec.ts b/packages/tm-core/src/common/utils/path-normalizer.spec.ts similarity index 100% rename from packages/tm-core/src/utils/path-normalizer.spec.ts rename to packages/tm-core/src/common/utils/path-normalizer.spec.ts diff --git a/packages/tm-core/src/utils/path-normalizer.ts b/packages/tm-core/src/common/utils/path-normalizer.ts similarity index 100% rename from packages/tm-core/src/utils/path-normalizer.ts rename to packages/tm-core/src/common/utils/path-normalizer.ts diff --git a/packages/tm-core/src/utils/run-id-generator.spec.ts b/packages/tm-core/src/common/utils/run-id-generator.spec.ts similarity index 100% rename from packages/tm-core/src/utils/run-id-generator.spec.ts rename to packages/tm-core/src/common/utils/run-id-generator.spec.ts diff --git a/packages/tm-core/src/utils/run-id-generator.ts b/packages/tm-core/src/common/utils/run-id-generator.ts similarity index 100% rename from packages/tm-core/src/utils/run-id-generator.ts rename to packages/tm-core/src/common/utils/run-id-generator.ts diff --git a/packages/tm-core/src/executors/index.ts b/packages/tm-core/src/executors/index.ts deleted file mode 100644 index 336e69e3..00000000 --- a/packages/tm-core/src/executors/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Public API for the executors module - */ - -export * from './types.js'; -export { BaseExecutor } from './base-executor.js'; -export { ClaudeExecutor } from './claude-executor.js'; -export { ExecutorFactory } from './executor-factory.js'; -export { - ExecutorService, - type ExecutorServiceOptions -} from './executor-service.js'; diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 6f81ef51..03577ac6 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -1,117 +1,127 @@ /** - * @fileoverview Main entry point for the tm-core package - * This file exports all public APIs from the core Task Master library + * @fileoverview Main entry point for @tm/core + * Provides unified access to all Task Master functionality through TmCore */ -// Export main facade -export { - TaskMasterCore, - createTaskMasterCore, - type TaskMasterCoreOptions, - type ListTasksResult, - type StartTaskOptions, - type StartTaskResult, - type ConflictCheckResult, - type ExportTasksOptions, - type ExportResult -} from './task-master-core.js'; +import type { TasksDomain } from './modules/tasks/tasks-domain.js'; -// Re-export types -export type * from './types/index.js'; +// ========== Primary API ========== -// Re-export interfaces (types only to avoid conflicts) -export type * from './interfaces/index.js'; +/** + * Create a new TmCore instance - The ONLY way to use tm-core + * + * @example + * ```typescript + * import { createTmCore } from '@tm/core'; + * + * const tmcore = await createTmCore({ + * projectPath: process.cwd() + * }); + * + * // Access domains + * await tmcore.auth.login({ ... }); + * const tasks = await tmcore.tasks.list(); + * await tmcore.workflow.start({ taskId: '1' }); + * await tmcore.git.commit('feat: add feature'); + * const config = tmcore.config.get('models.main'); + * ``` + */ +export { createTmCore, TmCore, type TmCoreOptions } from './tm-core.js'; -// Re-export constants -export * from './constants/index.js'; +// ========== Type Exports ========== -// Re-export providers -export * from './providers/index.js'; +// Common types that consumers need +export type * from './common/types/index.js'; -// Re-export storage (selectively to avoid conflicts) -export { - FileStorage, - ApiStorage, - StorageFactory, - type ApiStorageConfig -} from './storage/index.js'; -export { PlaceholderStorage, type StorageAdapter } from './storage/index.js'; +// Common interfaces +export type * from './common/interfaces/index.js'; -// Re-export parser -export * from './parser/index.js'; +// Constants +export * from './common/constants/index.js'; -// Re-export utilities -export * from './utils/index.js'; +// Errors +export * from './common/errors/index.js'; -// Re-export errors -export * from './errors/index.js'; +// ========== Domain-Specific Type Exports ========== -// Re-export entities -export { TaskEntity } from './entities/task.entity.js'; +// Task types +export type { + TaskListResult, + GetTaskListOptions +} from './modules/tasks/services/task-service.js'; -// Re-export authentication -export { - AuthManager, - AuthenticationError, - type AuthCredentials, - type OAuthFlowOptions, - type AuthConfig -} from './auth/index.js'; +export type { + StartTaskOptions, + StartTaskResult, + ConflictCheckResult +} from './modules/tasks/services/task-execution-service.js'; -// Re-export logger -export { getLogger, createLogger, setGlobalLogger } from './logger/index.js'; +export type { + PreflightResult, + CheckResult +} from './modules/tasks/services/preflight-checker.service.js'; -// Re-export executors -export * from './executors/index.js'; +// Task domain result types +export type TaskWithSubtaskResult = Awaited>; -// Re-export reports -export { - ComplexityReportManager, - type ComplexityReport, - type ComplexityReportMetadata, - type ComplexityAnalysis, - type TaskComplexityData -} from './reports/index.js'; +// Auth types +export type { + AuthCredentials, + OAuthFlowOptions, + UserContext +} from './modules/auth/types.js'; +export { AuthenticationError } from './modules/auth/types.js'; -// Re-export services -export { - PreflightChecker, - TaskLoaderService, - type CheckResult, - type PreflightResult, - type TaskValidationResult, - type ValidationErrorType, - type DependencyIssue -} from './services/index.js'; +// Workflow types +export type { + StartWorkflowOptions, + WorkflowStatus, + NextAction +} from './modules/workflow/services/workflow.service.js'; -// Re-export Git adapter -export { GitAdapter } from './git/git-adapter.js'; -export { - CommitMessageGenerator, - type CommitMessageOptions -} from './git/commit-message-generator.js'; - -// Re-export workflow orchestrator, state manager, activity logger, and types -export { WorkflowOrchestrator } from './workflow/workflow-orchestrator.js'; -export { WorkflowStateManager } from './workflow/workflow-state-manager.js'; -export { WorkflowActivityLogger } from './workflow/workflow-activity-logger.js'; export type { WorkflowPhase, TDDPhase, WorkflowContext, WorkflowState, - WorkflowEvent, - WorkflowEventData, - WorkflowEventListener, - SubtaskInfo, - TestResult, - WorkflowError -} from './workflow/types.js'; + TestResult +} from './modules/workflow/types.js'; -// Re-export workflow service -export { WorkflowService } from './services/workflow.service.js'; +// Git types +export type { CommitMessageOptions } from './modules/git/services/commit-message-generator.js'; + +// Integration types export type { - StartWorkflowOptions, - WorkflowStatus, - NextAction -} from './services/workflow.service.js'; + ExportTasksOptions, + ExportResult +} from './modules/integration/services/export.service.js'; + +// Reports types +export type { + ComplexityReport, + ComplexityReportMetadata, + ComplexityAnalysis, + TaskComplexityData +} from './modules/reports/types.js'; + +// ========== Advanced API (for CLI/Extension/MCP) ========== + +// Auth - Advanced +export { AuthManager } from './modules/auth/managers/auth-manager.js'; + +// Workflow - Advanced +export { WorkflowOrchestrator } from './modules/workflow/orchestrators/workflow-orchestrator.js'; +export { WorkflowStateManager } from './modules/workflow/managers/workflow-state-manager.js'; +export { WorkflowService } from './modules/workflow/services/workflow.service.js'; +export type { SubtaskInfo } from './modules/workflow/types.js'; + +// Git - Advanced +export { GitAdapter } from './modules/git/adapters/git-adapter.js'; +export { CommitMessageGenerator } from './modules/git/services/commit-message-generator.js'; + +// Tasks - Advanced +export { PreflightChecker } from './modules/tasks/services/preflight-checker.service.js'; +export { TaskLoaderService } from './modules/tasks/services/task-loader.service.js'; + +// Integration - Advanced +export { ExportService } from './modules/integration/services/export.service.js'; diff --git a/packages/tm-core/src/providers/index.ts b/packages/tm-core/src/modules/ai/index.ts similarity index 83% rename from packages/tm-core/src/providers/index.ts rename to packages/tm-core/src/modules/ai/index.ts index 6be8e3f2..43e51830 100644 --- a/packages/tm-core/src/providers/index.ts +++ b/packages/tm-core/src/modules/ai/index.ts @@ -3,7 +3,7 @@ */ // Export all from AI module -export * from './ai/index.js'; +export * from './providers/index.js'; // Storage providers will be exported here when implemented // export * from './storage/index.js'; diff --git a/packages/tm-core/src/interfaces/ai-provider.interface.ts b/packages/tm-core/src/modules/ai/interfaces/ai-provider.interface.ts similarity index 100% rename from packages/tm-core/src/interfaces/ai-provider.interface.ts rename to packages/tm-core/src/modules/ai/interfaces/ai-provider.interface.ts diff --git a/packages/tm-core/src/providers/ai/base-provider.ts b/packages/tm-core/src/modules/ai/providers/base-provider.ts similarity index 95% rename from packages/tm-core/src/providers/ai/base-provider.ts rename to packages/tm-core/src/modules/ai/providers/base-provider.ts index 3eb5d949..5db0477b 100644 --- a/packages/tm-core/src/providers/ai/base-provider.ts +++ b/packages/tm-core/src/modules/ai/providers/base-provider.ts @@ -6,12 +6,15 @@ import { ERROR_CODES, TaskMasterError -} from '../../errors/task-master-error.js'; +} from '../../../common/errors/task-master-error.js'; import type { AIOptions, AIResponse, - IAIProvider -} from '../../interfaces/ai-provider.interface.js'; + IAIProvider, + ProviderUsageStats, + ProviderInfo, + AIModel +} from '../interfaces/ai-provider.interface.js'; // Constants for retry logic const DEFAULT_MAX_RETRIES = 3; @@ -428,17 +431,10 @@ 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(): ProviderInfo; + abstract getAvailableModels(): AIModel[]; abstract validateCredentials(): Promise; - abstract getUsageStats(): Promise< - | import('../../interfaces/ai-provider.interface.js').ProviderUsageStats - | null - >; + abstract getUsageStats(): Promise; abstract initialize(): Promise; abstract close(): Promise; } diff --git a/packages/tm-core/src/providers/ai/index.ts b/packages/tm-core/src/modules/ai/providers/index.ts similarity index 100% rename from packages/tm-core/src/providers/ai/index.ts rename to packages/tm-core/src/modules/ai/providers/index.ts diff --git a/packages/tm-core/src/modules/auth/auth-domain.ts b/packages/tm-core/src/modules/auth/auth-domain.ts new file mode 100644 index 00000000..e137a66b --- /dev/null +++ b/packages/tm-core/src/modules/auth/auth-domain.ts @@ -0,0 +1,208 @@ +/** + * @fileoverview Auth Domain Facade + * Public API for authentication and authorization + */ + +import path from 'node:path'; +import { AuthManager } from './managers/auth-manager.js'; +import type { + AuthCredentials, + OAuthFlowOptions, + UserContext +} from './types.js'; +import type { + Organization, + Brief, + RemoteTask +} from './services/organization.service.js'; +import type { StorageType } from '../../common/types/index.js'; + +/** + * Display information for storage context + */ +export interface StorageDisplayInfo { + storageType: Exclude; + briefInfo?: { + briefId: string; + briefName: string; + orgSlug?: string; + webAppUrl?: string; + }; + filePath?: string; +} + +/** + * Auth Domain - Unified API for authentication operations + */ +export class AuthDomain { + private authManager: AuthManager; + + constructor() { + this.authManager = AuthManager.getInstance(); + } + + // ========== Authentication ========== + + /** + * Check if user is authenticated + */ + isAuthenticated(): boolean { + return this.authManager.isAuthenticated(); + } + + /** + * Get stored credentials + */ + getCredentials(): AuthCredentials | null { + return this.authManager.getCredentials(); + } + + /** + * Authenticate with OAuth flow + */ + async authenticateWithOAuth( + options?: OAuthFlowOptions + ): Promise { + return this.authManager.authenticateWithOAuth(options); + } + + /** + * Get OAuth authorization URL + */ + getAuthorizationUrl(): string | null { + return this.authManager.getAuthorizationUrl(); + } + + /** + * Refresh authentication token + */ + async refreshToken(): Promise { + return this.authManager.refreshToken(); + } + + /** + * Logout current user + */ + async logout(): Promise { + return this.authManager.logout(); + } + + // ========== User Context Management ========== + + /** + * Get current user context (org/brief selection) + */ + getContext(): UserContext | null { + return this.authManager.getContext(); + } + + /** + * Update user context + */ + updateContext(context: Partial): void { + return this.authManager.updateContext(context); + } + + /** + * Clear user context + */ + clearContext(): void { + return this.authManager.clearContext(); + } + + // ========== Organization Management ========== + + /** + * Get all organizations for the authenticated user + */ + async getOrganizations(): Promise { + return this.authManager.getOrganizations(); + } + + /** + * Get a specific organization by ID + */ + async getOrganization(orgId: string): Promise { + return this.authManager.getOrganization(orgId); + } + + /** + * Get all briefs for a specific organization + */ + async getBriefs(orgId: string): Promise { + return this.authManager.getBriefs(orgId); + } + + /** + * Get a specific brief by ID + */ + async getBrief(briefId: string): Promise { + return this.authManager.getBrief(briefId); + } + + /** + * Get all tasks for a specific brief + */ + async getTasks(briefId: string): Promise { + return this.authManager.getTasks(briefId); + } + + // ========== Display Information ========== + + /** + * Get storage display information for UI presentation + * Includes brief info for API storage, file path for file storage + * + * @param resolvedStorageType - The actual storage type being used at runtime. + * Get this from tmCore.tasks.getStorageType() + */ + getStorageDisplayInfo( + resolvedStorageType: 'file' | 'api' + ): StorageDisplayInfo { + if (resolvedStorageType === 'api') { + const context = this.getContext(); + if (context?.briefId && context?.briefName) { + return { + storageType: 'api', + briefInfo: { + briefId: context.briefId, + briefName: context.briefName, + orgSlug: context.orgSlug, + webAppUrl: this.getWebAppUrl() + } + }; + } + } + + // Default to file storage display + return { + storageType: 'file', + filePath: path.join('.taskmaster', 'tasks', 'tasks.json') + }; + } + + /** + * Get web app base URL from environment configuration + * @private + */ + private getWebAppUrl(): string | undefined { + const baseDomain = + process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN; + + if (!baseDomain) { + return undefined; + } + + // If it already includes protocol, use as-is + if (baseDomain.startsWith('http://') || baseDomain.startsWith('https://')) { + return baseDomain; + } + + // Otherwise, add protocol based on domain + if (baseDomain.includes('localhost') || baseDomain.includes('127.0.0.1')) { + return `http://${baseDomain}`; + } + + return `https://${baseDomain}`; + } +} diff --git a/packages/tm-core/src/auth/config.ts b/packages/tm-core/src/modules/auth/config.ts similarity index 100% rename from packages/tm-core/src/auth/config.ts rename to packages/tm-core/src/modules/auth/config.ts diff --git a/packages/tm-core/src/modules/auth/index.ts b/packages/tm-core/src/modules/auth/index.ts new file mode 100644 index 00000000..922519e9 --- /dev/null +++ b/packages/tm-core/src/modules/auth/index.ts @@ -0,0 +1,29 @@ +/** + * Authentication module exports + */ + +export { AuthDomain, type StorageDisplayInfo } from './auth-domain.js'; +export { AuthManager } from './managers/auth-manager.js'; +export { CredentialStore } from './services/credential-store.js'; +export { OAuthService } from './services/oauth-service.js'; +export { SupabaseSessionStorage } from './services/supabase-session-storage.js'; +export type { + Organization, + Brief, + RemoteTask +} from './services/organization.service.js'; + +export type { + AuthCredentials, + OAuthFlowOptions, + AuthConfig, + CliData, + UserContext +} from './types.js'; + +export { AuthenticationError } from './types.js'; + +export { + DEFAULT_AUTH_CONFIG, + getAuthConfig +} from './config.js'; diff --git a/packages/tm-core/src/auth/auth-manager.spec.ts b/packages/tm-core/src/modules/auth/managers/auth-manager.spec.ts similarity index 100% rename from packages/tm-core/src/auth/auth-manager.spec.ts rename to packages/tm-core/src/modules/auth/managers/auth-manager.spec.ts diff --git a/packages/tm-core/src/auth/auth-manager.ts b/packages/tm-core/src/modules/auth/managers/auth-manager.ts similarity index 96% rename from packages/tm-core/src/auth/auth-manager.ts rename to packages/tm-core/src/modules/auth/managers/auth-manager.ts index f525b36f..8aeb753f 100644 --- a/packages/tm-core/src/auth/auth-manager.ts +++ b/packages/tm-core/src/modules/auth/managers/auth-manager.ts @@ -8,17 +8,17 @@ import { AuthenticationError, AuthConfig, UserContext -} from './types.js'; -import { CredentialStore } from './credential-store.js'; -import { OAuthService } from './oauth-service.js'; -import { SupabaseAuthClient } from '../clients/supabase-client.js'; +} from '../types.js'; +import { CredentialStore } from '../services/credential-store.js'; +import { OAuthService } from '../services/oauth-service.js'; +import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js'; import { OrganizationService, type Organization, type Brief, type RemoteTask } from '../services/organization.service.js'; -import { getLogger } from '../logger/index.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * Authentication manager class diff --git a/packages/tm-core/src/auth/credential-store.spec.ts b/packages/tm-core/src/modules/auth/services/credential-store.spec.ts similarity index 100% rename from packages/tm-core/src/auth/credential-store.spec.ts rename to packages/tm-core/src/modules/auth/services/credential-store.spec.ts diff --git a/packages/tm-core/src/auth/credential-store.test.ts b/packages/tm-core/src/modules/auth/services/credential-store.test.ts similarity index 99% rename from packages/tm-core/src/auth/credential-store.test.ts rename to packages/tm-core/src/modules/auth/services/credential-store.test.ts index ce35b36b..4518278d 100644 --- a/packages/tm-core/src/auth/credential-store.test.ts +++ b/packages/tm-core/src/modules/auth/services/credential-store.test.ts @@ -3,9 +3,9 @@ */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { CredentialStore } from './credential-store.js'; -import { AuthenticationError } from './types.js'; -import type { AuthCredentials } from './types.js'; +import { CredentialStore } from '../services/credential-store.js'; +import { AuthenticationError } from '../types.js'; +import type { AuthCredentials } from '../types.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; diff --git a/packages/tm-core/src/auth/credential-store.ts b/packages/tm-core/src/modules/auth/services/credential-store.ts similarity index 98% rename from packages/tm-core/src/auth/credential-store.ts rename to packages/tm-core/src/modules/auth/services/credential-store.ts index fa33ee37..c0acbf78 100644 --- a/packages/tm-core/src/auth/credential-store.ts +++ b/packages/tm-core/src/modules/auth/services/credential-store.ts @@ -4,9 +4,9 @@ import fs from 'fs'; import path from 'path'; -import { AuthCredentials, AuthenticationError, AuthConfig } from './types.js'; -import { getAuthConfig } from './config.js'; -import { getLogger } from '../logger/index.js'; +import { AuthCredentials, AuthenticationError, AuthConfig } from '../types.js'; +import { getAuthConfig } from '../config.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * CredentialStore manages the persistence and retrieval of authentication credentials. diff --git a/packages/tm-core/src/auth/oauth-service.ts b/packages/tm-core/src/modules/auth/services/oauth-service.ts similarity index 96% rename from packages/tm-core/src/auth/oauth-service.ts rename to packages/tm-core/src/modules/auth/services/oauth-service.ts index 3977b5db..4a7dfe46 100644 --- a/packages/tm-core/src/auth/oauth-service.ts +++ b/packages/tm-core/src/modules/auth/services/oauth-service.ts @@ -12,12 +12,12 @@ import { OAuthFlowOptions, AuthConfig, CliData -} from './types.js'; -import { CredentialStore } from './credential-store.js'; -import { SupabaseAuthClient } from '../clients/supabase-client.js'; -import { getAuthConfig } from './config.js'; -import { getLogger } from '../logger/index.js'; -import packageJson from '../../../../package.json' with { type: 'json' }; +} from '../types.js'; +import { CredentialStore } from '../services/credential-store.js'; +import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js'; +import { getAuthConfig } from '../config.js'; +import { getLogger } from '../../../common/logger/index.js'; +import packageJson from '../../../../../../package.json' with { type: 'json' }; export class OAuthService { private logger = getLogger('OAuthService'); diff --git a/packages/tm-core/src/services/organization.service.ts b/packages/tm-core/src/modules/auth/services/organization.service.ts similarity index 97% rename from packages/tm-core/src/services/organization.service.ts rename to packages/tm-core/src/modules/auth/services/organization.service.ts index 8d126301..1868156a 100644 --- a/packages/tm-core/src/services/organization.service.ts +++ b/packages/tm-core/src/modules/auth/services/organization.service.ts @@ -4,9 +4,12 @@ */ import { SupabaseClient } from '@supabase/supabase-js'; -import { Database } from '../types/database.types.js'; -import { TaskMasterError, ERROR_CODES } from '../errors/task-master-error.js'; -import { getLogger } from '../logger/index.js'; +import { Database } from '../../../common/types/database.types.js'; +import { + TaskMasterError, + ERROR_CODES +} from '../../../common/errors/task-master-error.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * Organization data structure diff --git a/packages/tm-core/src/auth/supabase-session-storage.ts b/packages/tm-core/src/modules/auth/services/supabase-session-storage.ts similarity index 50% rename from packages/tm-core/src/auth/supabase-session-storage.ts rename to packages/tm-core/src/modules/auth/services/supabase-session-storage.ts index fe82e6ed..f425d232 100644 --- a/packages/tm-core/src/auth/supabase-session-storage.ts +++ b/packages/tm-core/src/modules/auth/services/supabase-session-storage.ts @@ -6,10 +6,10 @@ * auth.json credential storage, maintaining backward compatibility */ -import { SupportedStorage } from '@supabase/supabase-js'; +import type { SupportedStorage } from '@supabase/supabase-js'; import { CredentialStore } from './credential-store.js'; -import { AuthCredentials } from './types.js'; -import { getLogger } from '../logger/index.js'; +import type { AuthCredentials } from '../types.js'; +import { getLogger } from '../../../common/logger/index.js'; const STORAGE_KEY = 'sb-taskmaster-auth-token'; @@ -29,20 +29,14 @@ export class SupabaseSessionStorage implements SupportedStorage { const session = { access_token: credentials.token, refresh_token: credentials.refreshToken || '', - expires_at: credentials.expiresAt - ? Math.floor(new Date(credentials.expiresAt).getTime() / 1000) - : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour + // Don't default to arbitrary values - let Supabase handle refresh + ...(credentials.expiresAt && { + expires_at: Math.floor(new Date(credentials.expiresAt).getTime() / 1000) + }), token_type: 'bearer', user: { id: credentials.userId, - email: credentials.email || '', - aud: 'authenticated', - role: 'authenticated', - email_confirmed_at: new Date().toISOString(), - app_metadata: {}, - user_metadata: {}, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() + email: credentials.email || '' } }; return session; @@ -55,11 +49,14 @@ export class SupabaseSessionStorage implements SupportedStorage { sessionData: any ): Partial { try { - const session = JSON.parse(sessionData); + // Handle both string and object formats (Supabase may pass either) + const session = + typeof sessionData === 'string' ? JSON.parse(sessionData) : sessionData; + return { token: session.access_token, refreshToken: session.refresh_token, - userId: session.user?.id || 'unknown', + userId: session.user?.id, email: session.user?.email, expiresAt: session.expires_at ? new Date(session.expires_at * 1000).toISOString() @@ -78,21 +75,29 @@ export class SupabaseSessionStorage implements SupportedStorage { // Supabase uses a specific key pattern for sessions if (key === STORAGE_KEY || key.includes('auth-token')) { try { - const credentials = this.store.getCredentials({ allowExpired: true }); - if (credentials && credentials.token) { - // Build and return a session object from our stored credentials - const session = this.buildSessionFromCredentials(credentials); - return JSON.stringify(session); + // Get credentials and let Supabase handle expiry/refresh internally + const credentials = this.store.getCredentials(); + + // Only return a session if we have BOTH access token AND refresh token + // Supabase will handle refresh if session is expired + if (!credentials?.token || !credentials?.refreshToken) { + this.logger.debug('No valid credentials found'); + return null; } + + const session = this.buildSessionFromCredentials(credentials); + return JSON.stringify(session); } catch (error) { this.logger.error('Error getting session:', error); } } + // Return null if no valid session exists - Supabase expects this return null; } /** * Set item in storage - Supabase will store the session with a specific key + * CRITICAL: This is called during refresh token rotation - must be atomic */ setItem(key: string, value: string): void { // Only handle Supabase session keys @@ -102,21 +107,64 @@ export class SupabaseSessionStorage implements SupportedStorage { // Parse the session and update our credentials const sessionUpdates = this.parseSessionToCredentials(value); - const existingCredentials = this.store.getCredentials(); + const existingCredentials = this.store.getCredentials({ + allowExpired: true + }); - if (sessionUpdates.token) { - const updatedCredentials: AuthCredentials = { - ...existingCredentials, - ...sessionUpdates, - savedAt: new Date().toISOString(), - selectedContext: existingCredentials?.selectedContext - } as AuthCredentials; + // CRITICAL: Only save if we have both tokens - prevents partial session states + // Refresh token rotation means we MUST persist the new refresh token immediately + if (!sessionUpdates.token || !sessionUpdates.refreshToken) { + this.logger.warn( + 'Received incomplete session update - skipping save to prevent token rotation issues', + { + hasToken: !!sessionUpdates.token, + hasRefreshToken: !!sessionUpdates.refreshToken + } + ); + return; + } - this.store.saveCredentials(updatedCredentials); - this.logger.info( - 'Successfully saved refreshed credentials from Supabase' + // Log the refresh token rotation for debugging + const isRotation = + existingCredentials?.refreshToken !== sessionUpdates.refreshToken; + if (isRotation) { + this.logger.debug( + 'Refresh token rotated - storing new refresh token atomically' ); } + + // Build updated credentials - ATOMIC update of both tokens + const userId = sessionUpdates.userId ?? existingCredentials?.userId; + + // Runtime assertion: userId is required for AuthCredentials + if (!userId) { + this.logger.error( + 'Cannot save credentials: userId is missing from both session update and existing credentials' + ); + throw new Error('Invalid session state: userId is required'); + } + + const updatedCredentials: AuthCredentials = { + ...(existingCredentials ?? {}), + token: sessionUpdates.token, + refreshToken: sessionUpdates.refreshToken, + expiresAt: sessionUpdates.expiresAt, + userId, + email: sessionUpdates.email ?? existingCredentials?.email, + savedAt: new Date().toISOString(), + selectedContext: existingCredentials?.selectedContext + } as AuthCredentials; + + // Save synchronously to ensure atomicity during refresh + this.store.saveCredentials(updatedCredentials); + + this.logger.info( + 'Successfully saved refreshed credentials from Supabase', + { + tokenRotated: isRotation, + expiresAt: updatedCredentials.expiresAt + } + ); } catch (error) { this.logger.error('Error setting session:', error); } diff --git a/packages/tm-core/src/auth/types.ts b/packages/tm-core/src/modules/auth/types.ts similarity index 100% rename from packages/tm-core/src/auth/types.ts rename to packages/tm-core/src/modules/auth/types.ts diff --git a/packages/tm-core/src/modules/commands/index.ts b/packages/tm-core/src/modules/commands/index.ts new file mode 100644 index 00000000..148f6f54 --- /dev/null +++ b/packages/tm-core/src/modules/commands/index.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview Commands domain - Placeholder for future migration + * This module will handle command execution and orchestration + * when migrated from scripts/modules/commands.js + */ + +// TODO: Migrate commands.js from scripts/modules/ +// export * from './handlers/command-handler.js'; diff --git a/packages/tm-core/src/modules/config/config-domain.ts b/packages/tm-core/src/modules/config/config-domain.ts new file mode 100644 index 00000000..74931718 --- /dev/null +++ b/packages/tm-core/src/modules/config/config-domain.ts @@ -0,0 +1,116 @@ +/** + * @fileoverview Config Domain Facade + * Public API for configuration management + */ + +import type { ConfigManager } from './managers/config-manager.js'; +import type { + PartialConfiguration, + RuntimeStorageConfig +} from '../../common/interfaces/configuration.interface.js'; + +/** + * Config Domain - Unified API for configuration operations + */ +export class ConfigDomain { + constructor(private configManager: ConfigManager) {} + + // ========== Configuration Access ========== + + /** + * Get the full configuration + */ + getConfig(): PartialConfiguration { + return this.configManager.getConfig(); + } + + /** + * Get storage configuration + */ + getStorageConfig(): RuntimeStorageConfig { + return this.configManager.getStorageConfig(); + } + + /** + * Get model configuration + */ + getModelConfig() { + return this.configManager.getModelConfig(); + } + + /** + * Get response language + */ + getResponseLanguage(): string { + return this.configManager.getResponseLanguage(); + } + + /** + * Get project root path + */ + getProjectRoot(): string { + return this.configManager.getProjectRoot(); + } + + /** + * Check if API is explicitly configured + */ + isApiExplicitlyConfigured(): boolean { + return this.configManager.isApiExplicitlyConfigured(); + } + + // ========== Runtime State ========== + + /** + * Get the currently active tag + */ + getActiveTag(): string { + return this.configManager.getActiveTag(); + } + + /** + * Set the active tag + */ + async setActiveTag(tag: string): Promise { + return this.configManager.setActiveTag(tag); + } + + // ========== Configuration Updates ========== + + /** + * Update configuration + */ + async updateConfig(updates: PartialConfiguration): Promise { + return this.configManager.updateConfig(updates); + } + + /** + * Set response language + */ + async setResponseLanguage(language: string): Promise { + return this.configManager.setResponseLanguage(language); + } + + /** + * Save current configuration + */ + async saveConfig(): Promise { + return this.configManager.saveConfig(); + } + + /** + * Reset configuration to defaults + */ + async reset(): Promise { + return this.configManager.reset(); + } + + // ========== Utilities ========== + + /** + * Get configuration sources for debugging + */ + getConfigSources() { + return this.configManager.getConfigSources(); + } +} diff --git a/packages/tm-core/src/config/index.ts b/packages/tm-core/src/modules/config/index.ts similarity index 79% rename from packages/tm-core/src/config/index.ts rename to packages/tm-core/src/modules/config/index.ts index 2f759695..c0e49b80 100644 --- a/packages/tm-core/src/config/index.ts +++ b/packages/tm-core/src/modules/config/index.ts @@ -4,7 +4,7 @@ */ // Export the main ConfigManager -export { ConfigManager } from './config-manager.js'; +export { ConfigManager } from './managers/config-manager.js'; // Export all configuration services for advanced usage export { @@ -38,7 +38,7 @@ export type { ConfigProperty, IConfigurationFactory, IConfigurationManager -} from '../interfaces/configuration.interface.js'; +} from '../../common/interfaces/configuration.interface.js'; // Re-export default values -export { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js'; +export { DEFAULT_CONFIG_VALUES } from '../../common/interfaces/configuration.interface.js'; diff --git a/packages/tm-core/src/config/config-manager.spec.ts b/packages/tm-core/src/modules/config/managers/config-manager.spec.ts similarity index 90% rename from packages/tm-core/src/config/config-manager.spec.ts rename to packages/tm-core/src/modules/config/managers/config-manager.spec.ts index a214357c..d12ee03e 100644 --- a/packages/tm-core/src/config/config-manager.spec.ts +++ b/packages/tm-core/src/modules/config/managers/config-manager.spec.ts @@ -5,19 +5,19 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { ConfigManager } from './config-manager.js'; -import { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js'; -import { ConfigLoader } from './services/config-loader.service.js'; -import { ConfigMerger } from './services/config-merger.service.js'; -import { RuntimeStateManager } from './services/runtime-state-manager.service.js'; -import { ConfigPersistence } from './services/config-persistence.service.js'; -import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js'; +import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js'; +import { ConfigLoader } from '../services/config-loader.service.js'; +import { ConfigMerger } from '../services/config-merger.service.js'; +import { RuntimeStateManager } from '../services/runtime-state-manager.service.js'; +import { ConfigPersistence } from '../services/config-persistence.service.js'; +import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js'; // Mock all services -vi.mock('./services/config-loader.service.js'); -vi.mock('./services/config-merger.service.js'); -vi.mock('./services/runtime-state-manager.service.js'); -vi.mock('./services/config-persistence.service.js'); -vi.mock('./services/environment-config-provider.service.js'); +vi.mock('../services/config-loader.service.js'); +vi.mock('../services/config-merger.service.js'); +vi.mock('../services/runtime-state-manager.service.js'); +vi.mock('../services/config-persistence.service.js'); +vi.mock('../services/environment-config-provider.service.js'); describe('ConfigManager', () => { let manager: ConfigManager; @@ -361,23 +361,6 @@ describe('ConfigManager', () => { expect(sources).toEqual(mockSources); }); - - it('should return no-op function for watch (not implemented)', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - const callback = vi.fn(); - - const unsubscribe = manager.watch(callback); - - expect(warnSpy).toHaveBeenCalledWith( - 'Configuration watching not yet implemented' - ); - expect(unsubscribe).toBeInstanceOf(Function); - - // Calling unsubscribe should not throw - expect(() => unsubscribe()).not.toThrow(); - - warnSpy.mockRestore(); - }); }); describe('error handling', () => { diff --git a/packages/tm-core/src/config/config-manager.ts b/packages/tm-core/src/modules/config/managers/config-manager.ts similarity index 92% rename from packages/tm-core/src/config/config-manager.ts rename to packages/tm-core/src/modules/config/managers/config-manager.ts index 805d8193..1cce6157 100644 --- a/packages/tm-core/src/config/config-manager.ts +++ b/packages/tm-core/src/modules/config/managers/config-manager.ts @@ -9,16 +9,16 @@ import type { PartialConfiguration, RuntimeStorageConfig -} from '../interfaces/configuration.interface.js'; -import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../interfaces/configuration.interface.js'; -import { ConfigLoader } from './services/config-loader.service.js'; +} from '../../../common/interfaces/configuration.interface.js'; +import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../../../common/interfaces/configuration.interface.js'; +import { ConfigLoader } from '../services/config-loader.service.js'; import { ConfigMerger, CONFIG_PRECEDENCE -} from './services/config-merger.service.js'; -import { RuntimeStateManager } from './services/runtime-state-manager.service.js'; -import { ConfigPersistence } from './services/config-persistence.service.js'; -import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js'; +} from '../services/config-merger.service.js'; +import { RuntimeStateManager } from '../services/runtime-state-manager.service.js'; +import { ConfigPersistence } from '../services/config-persistence.service.js'; +import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js'; /** * ConfigManager orchestrates all configuration services diff --git a/packages/tm-core/src/config/services/config-loader.service.spec.ts b/packages/tm-core/src/modules/config/services/config-loader.service.spec.ts similarity index 97% rename from packages/tm-core/src/config/services/config-loader.service.spec.ts rename to packages/tm-core/src/modules/config/services/config-loader.service.spec.ts index d81ebf3a..ae88bb98 100644 --- a/packages/tm-core/src/config/services/config-loader.service.spec.ts +++ b/packages/tm-core/src/modules/config/services/config-loader.service.spec.ts @@ -5,7 +5,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { promises as fs } from 'node:fs'; import { ConfigLoader } from './config-loader.service.js'; -import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; +import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js'; vi.mock('node:fs', () => ({ promises: { diff --git a/packages/tm-core/src/config/services/config-loader.service.ts b/packages/tm-core/src/modules/config/services/config-loader.service.ts similarity index 94% rename from packages/tm-core/src/config/services/config-loader.service.ts rename to packages/tm-core/src/modules/config/services/config-loader.service.ts index 3988d1c0..2e35b521 100644 --- a/packages/tm-core/src/config/services/config-loader.service.ts +++ b/packages/tm-core/src/modules/config/services/config-loader.service.ts @@ -5,12 +5,12 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; -import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; -import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; +import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js'; +import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js'; import { ERROR_CODES, TaskMasterError -} from '../../errors/task-master-error.js'; +} from '../../../common/errors/task-master-error.js'; /** * ConfigLoader handles loading configuration from files diff --git a/packages/tm-core/src/config/services/config-merger.service.spec.ts b/packages/tm-core/src/modules/config/services/config-merger.service.spec.ts similarity index 100% rename from packages/tm-core/src/config/services/config-merger.service.spec.ts rename to packages/tm-core/src/modules/config/services/config-merger.service.spec.ts diff --git a/packages/tm-core/src/config/services/config-merger.service.ts b/packages/tm-core/src/modules/config/services/config-merger.service.ts similarity index 96% rename from packages/tm-core/src/config/services/config-merger.service.ts rename to packages/tm-core/src/modules/config/services/config-merger.service.ts index 0fd25dc3..970a34bc 100644 --- a/packages/tm-core/src/config/services/config-merger.service.ts +++ b/packages/tm-core/src/modules/config/services/config-merger.service.ts @@ -3,7 +3,7 @@ * Responsible for merging configurations from multiple sources with precedence */ -import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; +import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js'; /** * Configuration source with precedence diff --git a/packages/tm-core/src/config/services/config-persistence.service.spec.ts b/packages/tm-core/src/modules/config/services/config-persistence.service.spec.ts similarity index 100% rename from packages/tm-core/src/config/services/config-persistence.service.spec.ts rename to packages/tm-core/src/modules/config/services/config-persistence.service.spec.ts diff --git a/packages/tm-core/src/config/services/config-persistence.service.ts b/packages/tm-core/src/modules/config/services/config-persistence.service.ts similarity index 95% rename from packages/tm-core/src/config/services/config-persistence.service.ts rename to packages/tm-core/src/modules/config/services/config-persistence.service.ts index 393c4f9e..06b9f1a2 100644 --- a/packages/tm-core/src/config/services/config-persistence.service.ts +++ b/packages/tm-core/src/modules/config/services/config-persistence.service.ts @@ -5,12 +5,12 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; -import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; +import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js'; import { ERROR_CODES, TaskMasterError -} from '../../errors/task-master-error.js'; -import { getLogger } from '../../logger/index.js'; +} from '../../../common/errors/task-master-error.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * Persistence options diff --git a/packages/tm-core/src/config/services/environment-config-provider.service.spec.ts b/packages/tm-core/src/modules/config/services/environment-config-provider.service.spec.ts similarity index 100% rename from packages/tm-core/src/config/services/environment-config-provider.service.spec.ts rename to packages/tm-core/src/modules/config/services/environment-config-provider.service.spec.ts diff --git a/packages/tm-core/src/config/services/environment-config-provider.service.ts b/packages/tm-core/src/modules/config/services/environment-config-provider.service.ts similarity index 96% rename from packages/tm-core/src/config/services/environment-config-provider.service.ts rename to packages/tm-core/src/modules/config/services/environment-config-provider.service.ts index 067a9210..780975d1 100644 --- a/packages/tm-core/src/config/services/environment-config-provider.service.ts +++ b/packages/tm-core/src/modules/config/services/environment-config-provider.service.ts @@ -3,8 +3,8 @@ * Extracts configuration from environment variables */ -import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; -import { getLogger } from '../../logger/index.js'; +import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * Environment variable mapping definition diff --git a/packages/tm-core/src/config/services/index.ts b/packages/tm-core/src/modules/config/services/index.ts similarity index 100% rename from packages/tm-core/src/config/services/index.ts rename to packages/tm-core/src/modules/config/services/index.ts diff --git a/packages/tm-core/src/config/services/runtime-state-manager.service.spec.ts b/packages/tm-core/src/modules/config/services/runtime-state-manager.service.spec.ts similarity index 98% rename from packages/tm-core/src/config/services/runtime-state-manager.service.spec.ts rename to packages/tm-core/src/modules/config/services/runtime-state-manager.service.spec.ts index 6473f863..c6341e31 100644 --- a/packages/tm-core/src/config/services/runtime-state-manager.service.spec.ts +++ b/packages/tm-core/src/modules/config/services/runtime-state-manager.service.spec.ts @@ -5,7 +5,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { promises as fs } from 'node:fs'; import { RuntimeStateManager } from './runtime-state-manager.service.js'; -import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; +import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js'; vi.mock('node:fs', () => ({ promises: { diff --git a/packages/tm-core/src/config/services/runtime-state-manager.service.ts b/packages/tm-core/src/modules/config/services/runtime-state-manager.service.ts similarity index 94% rename from packages/tm-core/src/config/services/runtime-state-manager.service.ts rename to packages/tm-core/src/modules/config/services/runtime-state-manager.service.ts index ef1e14ed..3c633730 100644 --- a/packages/tm-core/src/config/services/runtime-state-manager.service.ts +++ b/packages/tm-core/src/modules/config/services/runtime-state-manager.service.ts @@ -8,9 +8,9 @@ import path from 'node:path'; import { ERROR_CODES, TaskMasterError -} from '../../errors/task-master-error.js'; -import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; -import { getLogger } from '../../logger/index.js'; +} from '../../../common/errors/task-master-error.js'; +import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * Runtime state data structure diff --git a/packages/tm-core/src/modules/dependencies/index.ts b/packages/tm-core/src/modules/dependencies/index.ts new file mode 100644 index 00000000..0b829585 --- /dev/null +++ b/packages/tm-core/src/modules/dependencies/index.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview Dependencies domain - Placeholder for future migration + * This module will handle dependency management, graphs, and validation + * when migrated from scripts/modules/dependency-manager.js + */ + +// TODO: Migrate dependency-manager.js from scripts/modules/ +// export * from './services/dependency-manager.js'; diff --git a/packages/tm-core/src/executors/base-executor.ts b/packages/tm-core/src/modules/execution/executors/base-executor.ts similarity index 93% rename from packages/tm-core/src/executors/base-executor.ts rename to packages/tm-core/src/modules/execution/executors/base-executor.ts index cc6f93e4..e8aad6a5 100644 --- a/packages/tm-core/src/executors/base-executor.ts +++ b/packages/tm-core/src/modules/execution/executors/base-executor.ts @@ -2,9 +2,9 @@ * Base executor class providing common functionality for all executors */ -import type { Task } from '../types/index.js'; -import type { ITaskExecutor, ExecutorType, ExecutionResult } from './types.js'; -import { getLogger } from '../logger/index.js'; +import type { Task } from '../../../common/types/index.js'; +import type { ITaskExecutor, ExecutorType, ExecutionResult } from '../types.js'; +import { getLogger } from '../../../common/logger/index.js'; export abstract class BaseExecutor implements ITaskExecutor { protected readonly logger = getLogger('BaseExecutor'); diff --git a/packages/tm-core/src/executors/claude-executor.ts b/packages/tm-core/src/modules/execution/executors/claude-executor.ts similarity index 95% rename from packages/tm-core/src/executors/claude-executor.ts rename to packages/tm-core/src/modules/execution/executors/claude-executor.ts index a3ddfebf..7482e749 100644 --- a/packages/tm-core/src/executors/claude-executor.ts +++ b/packages/tm-core/src/modules/execution/executors/claude-executor.ts @@ -3,13 +3,13 @@ */ import { spawn } from 'child_process'; -import type { Task } from '../types/index.js'; +import type { Task } from '../../../common/types/index.js'; import type { ExecutorType, ExecutionResult, ClaudeExecutorConfig -} from './types.js'; -import { BaseExecutor } from './base-executor.js'; +} from '../types.js'; +import { BaseExecutor } from '../executors/base-executor.js'; export class ClaudeExecutor extends BaseExecutor { private claudeConfig: ClaudeExecutorConfig; diff --git a/packages/tm-core/src/executors/executor-factory.ts b/packages/tm-core/src/modules/execution/executors/executor-factory.ts similarity index 90% rename from packages/tm-core/src/executors/executor-factory.ts rename to packages/tm-core/src/modules/execution/executors/executor-factory.ts index 8886bd3b..34ee51f2 100644 --- a/packages/tm-core/src/executors/executor-factory.ts +++ b/packages/tm-core/src/modules/execution/executors/executor-factory.ts @@ -2,9 +2,9 @@ * Factory for creating task executors */ -import type { ITaskExecutor, ExecutorOptions, ExecutorType } from './types.js'; -import { ClaudeExecutor } from './claude-executor.js'; -import { getLogger } from '../logger/index.js'; +import type { ITaskExecutor, ExecutorOptions, ExecutorType } from '../types.js'; +import { ClaudeExecutor } from '../executors/claude-executor.js'; +import { getLogger } from '../../../common/logger/index.js'; export class ExecutorFactory { private static logger = getLogger('ExecutorFactory'); diff --git a/packages/tm-core/src/modules/execution/index.ts b/packages/tm-core/src/modules/execution/index.ts new file mode 100644 index 00000000..75155d78 --- /dev/null +++ b/packages/tm-core/src/modules/execution/index.ts @@ -0,0 +1,12 @@ +/** + * Public API for the executors module + */ + +export * from './types.js'; +export { BaseExecutor } from './executors/base-executor.js'; +export { ClaudeExecutor } from './executors/claude-executor.js'; +export { ExecutorFactory } from './executors/executor-factory.js'; +export { + ExecutorService, + type ExecutorServiceOptions +} from './services/executor-service.js'; diff --git a/packages/tm-core/src/executors/executor-service.ts b/packages/tm-core/src/modules/execution/services/executor-service.ts similarity index 92% rename from packages/tm-core/src/executors/executor-service.ts rename to packages/tm-core/src/modules/execution/services/executor-service.ts index 69850a61..38fc1fae 100644 --- a/packages/tm-core/src/executors/executor-service.ts +++ b/packages/tm-core/src/modules/execution/services/executor-service.ts @@ -2,15 +2,15 @@ * Service for managing task execution */ -import type { Task } from '../types/index.js'; +import type { Task } from '../../../common/types/index.js'; import type { ITaskExecutor, ExecutorOptions, ExecutionResult, ExecutorType -} from './types.js'; -import { ExecutorFactory } from './executor-factory.js'; -import { getLogger } from '../logger/index.js'; +} from '../types.js'; +import { ExecutorFactory } from '../executors/executor-factory.js'; +import { getLogger } from '../../../common/logger/index.js'; export interface ExecutorServiceOptions { projectRoot: string; diff --git a/packages/tm-core/src/executors/types.ts b/packages/tm-core/src/modules/execution/types.ts similarity index 95% rename from packages/tm-core/src/executors/types.ts rename to packages/tm-core/src/modules/execution/types.ts index a94b5314..be7a6d90 100644 --- a/packages/tm-core/src/executors/types.ts +++ b/packages/tm-core/src/modules/execution/types.ts @@ -2,7 +2,7 @@ * Executor types and interfaces for Task Master */ -import type { Task } from '../types/index.js'; +import type { Task } from '../../common/types/index.js'; /** * Supported executor types diff --git a/packages/tm-core/src/git/git-adapter.test.ts b/packages/tm-core/src/modules/git/adapters/git-adapter.test.ts similarity index 100% rename from packages/tm-core/src/git/git-adapter.test.ts rename to packages/tm-core/src/modules/git/adapters/git-adapter.test.ts diff --git a/packages/tm-core/src/git/git-adapter.ts b/packages/tm-core/src/modules/git/adapters/git-adapter.ts similarity index 99% rename from packages/tm-core/src/git/git-adapter.ts rename to packages/tm-core/src/modules/git/adapters/git-adapter.ts index f331dde4..dd670e89 100644 --- a/packages/tm-core/src/git/git-adapter.ts +++ b/packages/tm-core/src/modules/git/adapters/git-adapter.ts @@ -5,7 +5,7 @@ * @module git-adapter */ -import { simpleGit, type SimpleGit } from 'simple-git'; +import { simpleGit, type SimpleGit, type StatusResult } from 'simple-git'; import fs from 'fs-extra'; import path from 'path'; @@ -216,14 +216,14 @@ export class GitAdapter { * Gets the detailed status of the working tree. * Returns raw status from simple-git with all file changes. * - * @returns {Promise} Detailed status object + * @returns {Promise} Detailed status object * * @example * const status = await git.getStatus(); * console.log('Modified files:', status.modified); * console.log('Staged files:', status.staged); */ - async getStatus(): Promise { + async getStatus(): Promise { return await this.git.status(); } diff --git a/packages/tm-core/src/modules/git/git-domain.ts b/packages/tm-core/src/modules/git/git-domain.ts new file mode 100644 index 00000000..b52f3d86 --- /dev/null +++ b/packages/tm-core/src/modules/git/git-domain.ts @@ -0,0 +1,247 @@ +/** + * @fileoverview Git Domain Facade + * Public API for Git operations + */ + +import { GitAdapter } from './adapters/git-adapter.js'; +import { CommitMessageGenerator } from './services/commit-message-generator.js'; +import type { CommitMessageOptions } from './services/commit-message-generator.js'; +import type { StatusResult } from 'simple-git'; + +/** + * Git Domain - Unified API for Git operations + */ +export class GitDomain { + private gitAdapter: GitAdapter; + private commitGenerator: CommitMessageGenerator; + + constructor(projectPath: string) { + this.gitAdapter = new GitAdapter(projectPath); + this.commitGenerator = new CommitMessageGenerator(); + } + + // ========== Repository Validation ========== + + /** + * Check if directory is a git repository + */ + async isGitRepository(): Promise { + return this.gitAdapter.isGitRepository(); + } + + /** + * Ensure we're in a valid git repository + */ + async ensureGitRepository(): Promise { + return this.gitAdapter.ensureGitRepository(); + } + + /** + * Get repository root path + */ + async getRepositoryRoot(): Promise { + return this.gitAdapter.getRepositoryRoot(); + } + + // ========== Working Tree Status ========== + + /** + * Check if working tree is clean + */ + async isWorkingTreeClean(): Promise { + return this.gitAdapter.isWorkingTreeClean(); + } + + /** + * Get git status + */ + async getStatus(): Promise { + return this.gitAdapter.getStatus(); + } + + /** + * Get status summary + */ + async getStatusSummary(): Promise<{ + isClean: boolean; + staged: number; + modified: number; + deleted: number; + untracked: number; + totalChanges: number; + }> { + return this.gitAdapter.getStatusSummary(); + } + + /** + * Check if there are uncommitted changes + */ + async hasUncommittedChanges(): Promise { + return this.gitAdapter.hasUncommittedChanges(); + } + + /** + * Check if there are staged changes + */ + async hasStagedChanges(): Promise { + return this.gitAdapter.hasStagedChanges(); + } + + // ========== Branch Operations ========== + + /** + * Get current branch name + */ + async getCurrentBranch(): Promise { + return this.gitAdapter.getCurrentBranch(); + } + + /** + * List all local branches + */ + async listBranches(): Promise { + return this.gitAdapter.listBranches(); + } + + /** + * Check if a branch exists + */ + async branchExists(branchName: string): Promise { + return this.gitAdapter.branchExists(branchName); + } + + /** + * Create a new branch + */ + async createBranch( + branchName: string, + options?: { checkout?: boolean } + ): Promise { + return this.gitAdapter.createBranch(branchName, options); + } + + /** + * Checkout an existing branch + */ + async checkoutBranch( + branchName: string, + options?: { force?: boolean } + ): Promise { + return this.gitAdapter.checkoutBranch(branchName, options); + } + + /** + * Create and checkout a new branch + */ + async createAndCheckoutBranch(branchName: string): Promise { + return this.gitAdapter.createAndCheckoutBranch(branchName); + } + + /** + * Delete a branch + */ + async deleteBranch( + branchName: string, + options?: { force?: boolean } + ): Promise { + return this.gitAdapter.deleteBranch(branchName, options); + } + + /** + * Get default branch name + */ + async getDefaultBranch(): Promise { + return this.gitAdapter.getDefaultBranch(); + } + + /** + * Check if on default branch + */ + async isOnDefaultBranch(): Promise { + return this.gitAdapter.isOnDefaultBranch(); + } + + // ========== Commit Operations ========== + + /** + * Stage files for commit + */ + async stageFiles(files: string[]): Promise { + return this.gitAdapter.stageFiles(files); + } + + /** + * Unstage files + */ + async unstageFiles(files: string[]): Promise { + return this.gitAdapter.unstageFiles(files); + } + + /** + * Create a commit + */ + async createCommit( + message: string, + options?: { + metadata?: Record; + allowEmpty?: boolean; + enforceNonDefaultBranch?: boolean; + force?: boolean; + } + ): Promise { + return this.gitAdapter.createCommit(message, options); + } + + /** + * Get commit log + */ + async getCommitLog(options?: { maxCount?: number }): Promise { + return this.gitAdapter.getCommitLog(options); + } + + /** + * Get last commit + */ + async getLastCommit(): Promise { + return this.gitAdapter.getLastCommit(); + } + + // ========== Remote Operations ========== + + /** + * Check if repository has remotes + */ + async hasRemote(): Promise { + return this.gitAdapter.hasRemote(); + } + + /** + * Get all configured remotes + */ + async getRemotes(): Promise { + return this.gitAdapter.getRemotes(); + } + + // ========== Commit Message Generation ========== + + /** + * Generate a conventional commit message + */ + generateCommitMessage(options: CommitMessageOptions): string { + return this.commitGenerator.generateMessage(options); + } + + /** + * Validate a conventional commit message + */ + validateCommitMessage(message: string) { + return this.commitGenerator.validateConventionalCommit(message); + } + + /** + * Parse a commit message + */ + parseCommitMessage(message: string) { + return this.commitGenerator.parseCommitMessage(message); + } +} diff --git a/packages/tm-core/src/git/index.ts b/packages/tm-core/src/modules/git/index.ts similarity index 69% rename from packages/tm-core/src/git/index.ts rename to packages/tm-core/src/modules/git/index.ts index 9987566a..042b30e3 100644 --- a/packages/tm-core/src/git/index.ts +++ b/packages/tm-core/src/modules/git/index.ts @@ -4,10 +4,10 @@ */ // Export GitAdapter -export { GitAdapter } from './git-adapter.js'; +export { GitAdapter } from './adapters/git-adapter.js'; // Export branch name utilities export { generateBranchName, sanitizeBranchName -} from './branch-name-generator.js'; +} from './services/branch-name-generator.js'; diff --git a/packages/tm-core/src/git/branch-name-generator.spec.ts b/packages/tm-core/src/modules/git/services/branch-name-generator.spec.ts similarity index 100% rename from packages/tm-core/src/git/branch-name-generator.spec.ts rename to packages/tm-core/src/modules/git/services/branch-name-generator.spec.ts diff --git a/packages/tm-core/src/git/branch-name-generator.ts b/packages/tm-core/src/modules/git/services/branch-name-generator.ts similarity index 100% rename from packages/tm-core/src/git/branch-name-generator.ts rename to packages/tm-core/src/modules/git/services/branch-name-generator.ts diff --git a/packages/tm-core/src/git/commit-message-generator.test.ts b/packages/tm-core/src/modules/git/services/commit-message-generator.test.ts similarity index 100% rename from packages/tm-core/src/git/commit-message-generator.test.ts rename to packages/tm-core/src/modules/git/services/commit-message-generator.test.ts diff --git a/packages/tm-core/src/git/commit-message-generator.ts b/packages/tm-core/src/modules/git/services/commit-message-generator.ts similarity index 100% rename from packages/tm-core/src/git/commit-message-generator.ts rename to packages/tm-core/src/modules/git/services/commit-message-generator.ts diff --git a/packages/tm-core/src/git/scope-detector.test.ts b/packages/tm-core/src/modules/git/services/scope-detector.test.ts similarity index 100% rename from packages/tm-core/src/git/scope-detector.test.ts rename to packages/tm-core/src/modules/git/services/scope-detector.test.ts diff --git a/packages/tm-core/src/git/scope-detector.ts b/packages/tm-core/src/modules/git/services/scope-detector.ts similarity index 100% rename from packages/tm-core/src/git/scope-detector.ts rename to packages/tm-core/src/modules/git/services/scope-detector.ts diff --git a/packages/tm-core/src/git/template-engine.test.ts b/packages/tm-core/src/modules/git/services/template-engine.test.ts similarity index 100% rename from packages/tm-core/src/git/template-engine.test.ts rename to packages/tm-core/src/modules/git/services/template-engine.test.ts diff --git a/packages/tm-core/src/git/template-engine.ts b/packages/tm-core/src/modules/git/services/template-engine.ts similarity index 100% rename from packages/tm-core/src/git/template-engine.ts rename to packages/tm-core/src/modules/git/services/template-engine.ts diff --git a/packages/tm-core/src/clients/index.ts b/packages/tm-core/src/modules/integration/clients/index.ts similarity index 100% rename from packages/tm-core/src/clients/index.ts rename to packages/tm-core/src/modules/integration/clients/index.ts diff --git a/packages/tm-core/src/clients/supabase-client.ts b/packages/tm-core/src/modules/integration/clients/supabase-client.ts similarity index 96% rename from packages/tm-core/src/clients/supabase-client.ts rename to packages/tm-core/src/modules/integration/clients/supabase-client.ts index a2d1dcab..3fede755 100644 --- a/packages/tm-core/src/clients/supabase-client.ts +++ b/packages/tm-core/src/modules/integration/clients/supabase-client.ts @@ -8,10 +8,10 @@ import { User, Session } from '@supabase/supabase-js'; -import { AuthenticationError } from '../auth/types.js'; -import { getLogger } from '../logger/index.js'; -import { SupabaseSessionStorage } from '../auth/supabase-session-storage.js'; -import { CredentialStore } from '../auth/credential-store.js'; +import { AuthenticationError } from '../../auth/types.js'; +import { getLogger } from '../../../common/logger/index.js'; +import { SupabaseSessionStorage } from '../../auth/services/supabase-session-storage.js'; +import { CredentialStore } from '../../auth/services/credential-store.js'; export class SupabaseAuthClient { private client: SupabaseJSClient | null = null; diff --git a/packages/tm-core/src/modules/integration/integration-domain.ts b/packages/tm-core/src/modules/integration/integration-domain.ts new file mode 100644 index 00000000..d7325a9f --- /dev/null +++ b/packages/tm-core/src/modules/integration/integration-domain.ts @@ -0,0 +1,34 @@ +/** + * @fileoverview Integration Domain Facade + * Public API for integration with external systems + */ + +import type { ConfigManager } from '../config/managers/config-manager.js'; +import { AuthManager } from '../auth/managers/auth-manager.js'; +import { ExportService } from './services/export.service.js'; +import type { + ExportTasksOptions, + ExportResult +} from './services/export.service.js'; + +/** + * Integration Domain - Unified API for external system integration + */ +export class IntegrationDomain { + private exportService: ExportService; + + constructor(configManager: ConfigManager) { + // Get singleton AuthManager instance + const authManager = AuthManager.getInstance(); + this.exportService = new ExportService(configManager, authManager); + } + + // ========== Export Operations ========== + + /** + * Export tasks to external systems (e.g., Hamster briefs) + */ + async exportTasks(options: ExportTasksOptions): Promise { + return this.exportService.exportTasks(options); + } +} diff --git a/packages/tm-core/src/services/export.service.ts b/packages/tm-core/src/modules/integration/services/export.service.ts similarity index 96% rename from packages/tm-core/src/services/export.service.ts rename to packages/tm-core/src/modules/integration/services/export.service.ts index 94e3bfe6..6907c73b 100644 --- a/packages/tm-core/src/services/export.service.ts +++ b/packages/tm-core/src/modules/integration/services/export.service.ts @@ -3,12 +3,15 @@ * Core service for exporting tasks to external systems (e.g., Hamster briefs) */ -import type { Task, TaskStatus } from '../types/index.js'; -import type { UserContext } from '../auth/types.js'; -import { ConfigManager } from '../config/config-manager.js'; -import { AuthManager } from '../auth/auth-manager.js'; -import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; -import { FileStorage } from '../storage/file-storage/index.js'; +import type { Task, TaskStatus } from '../../../common/types/index.js'; +import type { UserContext } from '../../auth/types.js'; +import { ConfigManager } from '../../config/managers/config-manager.js'; +import { AuthManager } from '../../auth/managers/auth-manager.js'; +import { + ERROR_CODES, + TaskMasterError +} from '../../../common/errors/task-master-error.js'; +import { FileStorage } from '../../storage/adapters/file-storage/index.js'; // Type definitions for the bulk API response interface TaskImportResult { diff --git a/packages/tm-core/src/reports/index.ts b/packages/tm-core/src/modules/reports/index.ts similarity index 67% rename from packages/tm-core/src/reports/index.ts rename to packages/tm-core/src/modules/reports/index.ts index 6f6e48cd..a0ac01cb 100644 --- a/packages/tm-core/src/reports/index.ts +++ b/packages/tm-core/src/modules/reports/index.ts @@ -2,7 +2,7 @@ * @fileoverview Reports module exports */ -export { ComplexityReportManager } from './complexity-report-manager.js'; +export { ComplexityReportManager } from './managers/complexity-report-manager.js'; export type { ComplexityReport, ComplexityReportMetadata, diff --git a/packages/tm-core/src/reports/complexity-report-manager.ts b/packages/tm-core/src/modules/reports/managers/complexity-report-manager.ts similarity index 98% rename from packages/tm-core/src/reports/complexity-report-manager.ts rename to packages/tm-core/src/modules/reports/managers/complexity-report-manager.ts index e55758a5..ca1fadd1 100644 --- a/packages/tm-core/src/reports/complexity-report-manager.ts +++ b/packages/tm-core/src/modules/reports/managers/complexity-report-manager.ts @@ -9,8 +9,8 @@ import type { ComplexityReport, ComplexityAnalysis, TaskComplexityData -} from './types.js'; -import { getLogger } from '../logger/index.js'; +} from '../types.js'; +import { getLogger } from '../../../common/logger/index.js'; const logger = getLogger('ComplexityReportManager'); diff --git a/packages/tm-core/src/reports/types.ts b/packages/tm-core/src/modules/reports/types.ts similarity index 100% rename from packages/tm-core/src/reports/types.ts rename to packages/tm-core/src/modules/reports/types.ts diff --git a/packages/tm-core/src/storage/activity-logger.ts b/packages/tm-core/src/modules/storage/adapters/activity-logger.ts similarity index 100% rename from packages/tm-core/src/storage/activity-logger.ts rename to packages/tm-core/src/modules/storage/adapters/activity-logger.ts diff --git a/packages/tm-core/src/storage/api-storage.ts b/packages/tm-core/src/modules/storage/adapters/api-storage.ts similarity index 97% rename from packages/tm-core/src/storage/api-storage.ts rename to packages/tm-core/src/modules/storage/adapters/api-storage.ts index 0798e38e..5165211d 100644 --- a/packages/tm-core/src/storage/api-storage.ts +++ b/packages/tm-core/src/modules/storage/adapters/api-storage.ts @@ -8,18 +8,21 @@ import type { StorageStats, UpdateStatusResult, LoadTasksOptions -} from '../interfaces/storage.interface.js'; +} from '../../../common/interfaces/storage.interface.js'; import type { Task, TaskMetadata, TaskTag, TaskStatus -} from '../types/index.js'; -import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; -import { TaskRepository } from '../repositories/task-repository.interface.js'; -import { SupabaseTaskRepository } from '../repositories/supabase/index.js'; +} from '../../../common/types/index.js'; +import { + ERROR_CODES, + TaskMasterError +} from '../../../common/errors/task-master-error.js'; +import { TaskRepository } from '../../tasks/repositories/task-repository.interface.js'; +import { SupabaseTaskRepository } from '../../tasks/repositories/supabase/index.js'; import { SupabaseClient } from '@supabase/supabase-js'; -import { AuthManager } from '../auth/auth-manager.js'; +import { AuthManager } from '../../auth/managers/auth-manager.js'; /** * API storage configuration @@ -122,7 +125,7 @@ export class ApiStorage implements IStorage { /** * Get the storage type */ - getType(): 'api' { + getStorageType(): 'api' { return 'api'; } diff --git a/packages/tm-core/src/storage/file-storage/file-operations.ts b/packages/tm-core/src/modules/storage/adapters/file-storage/file-operations.ts similarity index 100% rename from packages/tm-core/src/storage/file-storage/file-operations.ts rename to packages/tm-core/src/modules/storage/adapters/file-storage/file-operations.ts diff --git a/packages/tm-core/src/storage/file-storage/file-storage.ts b/packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts similarity index 98% rename from packages/tm-core/src/storage/file-storage/file-storage.ts rename to packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts index b44039b6..b01bc124 100644 --- a/packages/tm-core/src/storage/file-storage/file-storage.ts +++ b/packages/tm-core/src/modules/storage/adapters/file-storage/file-storage.ts @@ -2,17 +2,21 @@ * @fileoverview Refactored file-based storage implementation for Task Master */ -import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js'; +import type { + Task, + TaskMetadata, + TaskStatus +} from '../../../../common/types/index.js'; import type { IStorage, StorageStats, UpdateStatusResult, LoadTasksOptions -} from '../../interfaces/storage.interface.js'; +} from '../../../../common/interfaces/storage.interface.js'; import { FormatHandler } from './format-handler.js'; import { FileOperations } from './file-operations.js'; import { PathResolver } from './path-resolver.js'; -import { ComplexityReportManager } from '../../reports/complexity-report-manager.js'; +import { ComplexityReportManager } from '../../../reports/managers/complexity-report-manager.js'; /** * File-based storage implementation using a single tasks.json file with separated concerns @@ -47,7 +51,7 @@ export class FileStorage implements IStorage { /** * Get the storage type */ - getType(): 'file' { + getStorageType(): 'file' { return 'file'; } diff --git a/packages/tm-core/src/storage/file-storage/format-handler.ts b/packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts similarity index 98% rename from packages/tm-core/src/storage/file-storage/format-handler.ts rename to packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts index bc60b077..2e072fa1 100644 --- a/packages/tm-core/src/storage/file-storage/format-handler.ts +++ b/packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts @@ -2,7 +2,7 @@ * @fileoverview Format handler for task storage files */ -import type { Task, TaskMetadata } from '../../types/index.js'; +import type { Task, TaskMetadata } from '../../../../common/types/index.js'; export interface FileStorageData { tasks: Task[]; diff --git a/packages/tm-core/src/storage/file-storage/index.ts b/packages/tm-core/src/modules/storage/adapters/file-storage/index.ts similarity index 100% rename from packages/tm-core/src/storage/file-storage/index.ts rename to packages/tm-core/src/modules/storage/adapters/file-storage/index.ts diff --git a/packages/tm-core/src/storage/file-storage/path-resolver.ts b/packages/tm-core/src/modules/storage/adapters/file-storage/path-resolver.ts similarity index 100% rename from packages/tm-core/src/storage/file-storage/path-resolver.ts rename to packages/tm-core/src/modules/storage/adapters/file-storage/path-resolver.ts diff --git a/packages/tm-core/src/storage/index.ts b/packages/tm-core/src/modules/storage/index.ts similarity index 79% rename from packages/tm-core/src/storage/index.ts rename to packages/tm-core/src/modules/storage/index.ts index 2ee9a1d3..0a4ece3d 100644 --- a/packages/tm-core/src/storage/index.ts +++ b/packages/tm-core/src/modules/storage/index.ts @@ -4,9 +4,9 @@ */ // Export storage implementations -export { FileStorage } from './file-storage/index.js'; -export { ApiStorage, type ApiStorageConfig } from './api-storage.js'; -export { StorageFactory } from './storage-factory.js'; +export { FileStorage } from './adapters/file-storage/index.js'; +export { ApiStorage, type ApiStorageConfig } from './adapters/api-storage.js'; +export { StorageFactory } from './services/storage-factory.js'; // Export activity logger export { @@ -15,13 +15,13 @@ export { filterActivityLog, type ActivityEvent, type ActivityFilter -} from './activity-logger.js'; +} from './adapters/activity-logger.js'; // Export storage interface and types export type { IStorage, StorageStats -} from '../interfaces/storage.interface.js'; +} from '../../common/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/modules/storage/services/storage-factory.ts similarity index 92% rename from packages/tm-core/src/storage/storage-factory.ts rename to packages/tm-core/src/modules/storage/services/storage-factory.ts index a08d3df2..39772e91 100644 --- a/packages/tm-core/src/storage/storage-factory.ts +++ b/packages/tm-core/src/modules/storage/services/storage-factory.ts @@ -2,18 +2,21 @@ * @fileoverview Storage factory for creating appropriate storage implementations */ -import type { IStorage } from '../interfaces/storage.interface.js'; +import type { IStorage } from '../../../common/interfaces/storage.interface.js'; import type { IConfiguration, RuntimeStorageConfig, StorageSettings -} from '../interfaces/configuration.interface.js'; -import { FileStorage } from './file-storage/index.js'; -import { ApiStorage } from './api-storage.js'; -import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; -import { AuthManager } from '../auth/auth-manager.js'; -import { getLogger } from '../logger/index.js'; -import { SupabaseAuthClient } from '../clients/supabase-client.js'; +} from '../../../common/interfaces/configuration.interface.js'; +import { FileStorage } from '../adapters/file-storage/index.js'; +import { ApiStorage } from '../adapters/api-storage.js'; +import { + ERROR_CODES, + TaskMasterError +} from '../../../common/errors/task-master-error.js'; +import { AuthManager } from '../../auth/managers/auth-manager.js'; +import { getLogger } from '../../../common/logger/index.js'; +import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js'; /** * Factory for creating storage implementations based on configuration diff --git a/packages/tm-core/src/entities/task.entity.ts b/packages/tm-core/src/modules/tasks/entities/task.entity.ts similarity index 97% rename from packages/tm-core/src/entities/task.entity.ts rename to packages/tm-core/src/modules/tasks/entities/task.entity.ts index 8737615f..3e17f83e 100644 --- a/packages/tm-core/src/entities/task.entity.ts +++ b/packages/tm-core/src/modules/tasks/entities/task.entity.ts @@ -2,13 +2,13 @@ * @fileoverview Task entity with business rules and domain logic */ -import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; +import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js'; import type { Subtask, Task, TaskPriority, TaskStatus -} from '../types/index.js'; +} from '../../../common/types/index.js'; /** * Task entity representing a task with business logic diff --git a/packages/tm-core/src/parser/index.ts b/packages/tm-core/src/modules/tasks/parser/index.ts similarity index 94% rename from packages/tm-core/src/parser/index.ts rename to packages/tm-core/src/modules/tasks/parser/index.ts index 199ffb60..c5755d3e 100644 --- a/packages/tm-core/src/parser/index.ts +++ b/packages/tm-core/src/modules/tasks/parser/index.ts @@ -3,7 +3,7 @@ * This file exports all parsing-related classes and functions */ -import type { PlaceholderTask } from '../types/index.js'; +import type { PlaceholderTask } from '../../../common/types/index.js'; // Parser implementations will be defined here // export * from './prd-parser.js'; diff --git a/packages/tm-core/src/repositories/supabase/dependency-fetcher.ts b/packages/tm-core/src/modules/tasks/repositories/supabase/dependency-fetcher.ts similarity index 91% rename from packages/tm-core/src/repositories/supabase/dependency-fetcher.ts rename to packages/tm-core/src/modules/tasks/repositories/supabase/dependency-fetcher.ts index 0f0ef97a..355944b5 100644 --- a/packages/tm-core/src/repositories/supabase/dependency-fetcher.ts +++ b/packages/tm-core/src/modules/tasks/repositories/supabase/dependency-fetcher.ts @@ -1,6 +1,6 @@ import { SupabaseClient } from '@supabase/supabase-js'; -import { Database } from '../../types/database.types.js'; -import { DependencyWithDisplayId } from '../../types/repository-types.js'; +import { Database } from '../../../../common/types/database.types.js'; +import { DependencyWithDisplayId } from '../../../../common/types/repository-types.js'; /** * Handles fetching and processing of task dependencies with display_ids diff --git a/packages/tm-core/src/repositories/supabase/index.ts b/packages/tm-core/src/modules/tasks/repositories/supabase/index.ts similarity index 100% rename from packages/tm-core/src/repositories/supabase/index.ts rename to packages/tm-core/src/modules/tasks/repositories/supabase/index.ts diff --git a/packages/tm-core/src/repositories/supabase/supabase-task-repository.ts b/packages/tm-core/src/modules/tasks/repositories/supabase/supabase-task-repository.ts similarity index 91% rename from packages/tm-core/src/repositories/supabase/supabase-task-repository.ts rename to packages/tm-core/src/modules/tasks/repositories/supabase/supabase-task-repository.ts index faf9fbd0..62d4a0b1 100644 --- a/packages/tm-core/src/repositories/supabase/supabase-task-repository.ts +++ b/packages/tm-core/src/modules/tasks/repositories/supabase/supabase-task-repository.ts @@ -1,14 +1,14 @@ import { SupabaseClient } from '@supabase/supabase-js'; -import { Task } from '../../types/index.js'; -import { Database, Json } from '../../types/database.types.js'; -import { TaskMapper } from '../../mappers/TaskMapper.js'; -import { AuthManager } from '../../auth/auth-manager.js'; +import { Task } from '../../../../common/types/index.js'; +import { Database, Json } from '../../../../common/types/database.types.js'; +import { TaskMapper } from '../../../../common/mappers/TaskMapper.js'; +import { AuthManager } from '../../../auth/managers/auth-manager.js'; import { DependencyFetcher } from './dependency-fetcher.js'; import { TaskWithRelations, TaskDatabaseUpdate -} from '../../types/repository-types.js'; -import { LoadTasksOptions } from '../../interfaces/storage.interface.js'; +} from '../../../../common/types/repository-types.js'; +import { LoadTasksOptions } from '../../../../common/interfaces/storage.interface.js'; import { z } from 'zod'; // Zod schema for task status validation @@ -47,8 +47,8 @@ export class SupabaseTaskRepository { * Gets the current brief ID from auth context * @throws {Error} If no brief is selected */ - private async getBriefIdOrThrow(): Promise { - const context = await this.authManager.getContext(); + private getBriefIdOrThrow(): string { + const context = this.authManager.getContext(); if (!context?.briefId) { throw new Error( 'No brief selected. Please select a brief first using: tm context brief' @@ -61,7 +61,7 @@ export class SupabaseTaskRepository { _projectId?: string, options?: LoadTasksOptions ): Promise { - const briefId = await this.getBriefIdOrThrow(); + const briefId = this.getBriefIdOrThrow(); // Build query with filters let query = this.supabase @@ -114,7 +114,7 @@ export class SupabaseTaskRepository { } async getTask(_projectId: string, taskId: string): Promise { - const briefId = await this.getBriefIdOrThrow(); + const briefId = this.getBriefIdOrThrow(); const { data, error } = await this.supabase .from('tasks') @@ -157,7 +157,7 @@ export class SupabaseTaskRepository { taskId: string, updates: Partial ): Promise { - const briefId = await this.getBriefIdOrThrow(); + const briefId = this.getBriefIdOrThrow(); // Validate updates using Zod schema try { diff --git a/packages/tm-core/src/repositories/task-repository.interface.ts b/packages/tm-core/src/modules/tasks/repositories/task-repository.interface.ts similarity index 88% rename from packages/tm-core/src/repositories/task-repository.interface.ts rename to packages/tm-core/src/modules/tasks/repositories/task-repository.interface.ts index 0d5928d3..672f4e7f 100644 --- a/packages/tm-core/src/repositories/task-repository.interface.ts +++ b/packages/tm-core/src/modules/tasks/repositories/task-repository.interface.ts @@ -1,5 +1,5 @@ -import { Task, TaskTag } from '../types/index.js'; -import { LoadTasksOptions } from '../interfaces/storage.interface.js'; +import { Task, TaskTag } from '../../../common/types/index.js'; +import { LoadTasksOptions } from '../../../common/interfaces/storage.interface.js'; export interface TaskRepository { // Task operations diff --git a/packages/tm-core/src/services/preflight-checker.service.ts b/packages/tm-core/src/modules/tasks/services/preflight-checker.service.ts similarity index 98% rename from packages/tm-core/src/services/preflight-checker.service.ts rename to packages/tm-core/src/modules/tasks/services/preflight-checker.service.ts index abb8870b..863c2d08 100644 --- a/packages/tm-core/src/services/preflight-checker.service.ts +++ b/packages/tm-core/src/modules/tasks/services/preflight-checker.service.ts @@ -6,12 +6,12 @@ import { readFileSync, existsSync, readdirSync } from 'fs'; import { join } from 'path'; import { execSync } from 'child_process'; -import { getLogger } from '../logger/factory.js'; +import { getLogger } from '../../../common/logger/factory.js'; import { isGitRepository, isGhCliAvailable, getDefaultBranch -} from '../utils/git-utils.js'; +} from '../../../common/utils/git-utils.js'; const logger = getLogger('PreflightChecker'); diff --git a/packages/tm-core/src/services/task-execution-service.ts b/packages/tm-core/src/modules/tasks/services/task-execution-service.ts similarity index 99% rename from packages/tm-core/src/services/task-execution-service.ts rename to packages/tm-core/src/modules/tasks/services/task-execution-service.ts index 347e0518..19d53cad 100644 --- a/packages/tm-core/src/services/task-execution-service.ts +++ b/packages/tm-core/src/modules/tasks/services/task-execution-service.ts @@ -3,7 +3,7 @@ * Extracted from CLI start command to be reusable across CLI and extension */ -import type { Task } from '../types/index.js'; +import type { Task } from '../../../common/types/index.js'; import type { TaskService } from './task-service.js'; export interface StartTaskOptions { diff --git a/packages/tm-core/src/services/task-loader.service.ts b/packages/tm-core/src/modules/tasks/services/task-loader.service.ts similarity index 91% rename from packages/tm-core/src/services/task-loader.service.ts rename to packages/tm-core/src/modules/tasks/services/task-loader.service.ts index 4c98a34b..9c3986f3 100644 --- a/packages/tm-core/src/services/task-loader.service.ts +++ b/packages/tm-core/src/modules/tasks/services/task-loader.service.ts @@ -3,10 +3,9 @@ * Loads and validates tasks for autopilot execution */ -import type { Task, Subtask, TaskStatus } from '../types/index.js'; -import { TaskService } from './task-service.js'; -import { ConfigManager } from '../config/config-manager.js'; -import { getLogger } from '../logger/factory.js'; +import type { Task, Subtask, TaskStatus } from '../../../common/types/index.js'; +import type { TaskService } from './task-service.js'; +import { getLogger } from '../../../common/logger/factory.js'; const logger = getLogger('TaskLoader'); @@ -57,25 +56,13 @@ export interface DependencyIssue { * TaskLoaderService loads and validates tasks for autopilot execution */ export class TaskLoaderService { - private taskService: TaskService | null = null; - private projectRoot: string; + private taskService: TaskService; - constructor(projectRoot: string) { - if (!projectRoot) { - throw new Error('projectRoot is required for TaskLoaderService'); + constructor(taskService: TaskService) { + if (!taskService) { + throw new Error('taskService is required for TaskLoaderService'); } - this.projectRoot = projectRoot; - } - - /** - * Ensure TaskService is initialized - */ - private async ensureInitialized(): Promise { - if (this.taskService) return; - - const configManager = await ConfigManager.create(this.projectRoot); - this.taskService = new TaskService(configManager); - await this.taskService.initialize(); + this.taskService = taskService; } /** @@ -133,10 +120,6 @@ export class TaskLoaderService { */ private async loadTask(taskId: string): Promise { try { - await this.ensureInitialized(); - if (!this.taskService) { - throw new Error('TaskService initialization failed'); - } return await this.taskService.getTask(taskId); } catch (error) { logger.error(`Failed to load task ${taskId}:`, error); diff --git a/packages/tm-core/src/services/task-service.ts b/packages/tm-core/src/modules/tasks/services/task-service.ts similarity index 95% rename from packages/tm-core/src/services/task-service.ts rename to packages/tm-core/src/modules/tasks/services/task-service.ts index a30289bf..b29f0783 100644 --- a/packages/tm-core/src/services/task-service.ts +++ b/packages/tm-core/src/modules/tasks/services/task-service.ts @@ -8,13 +8,13 @@ import type { TaskFilter, TaskStatus, StorageType -} from '../types/index.js'; -import type { IStorage } from '../interfaces/storage.interface.js'; -import { ConfigManager } from '../config/config-manager.js'; -import { StorageFactory } from '../storage/storage-factory.js'; +} from '../../../common/types/index.js'; +import type { IStorage } from '../../../common/interfaces/storage.interface.js'; +import { ConfigManager } from '../../config/managers/config-manager.js'; +import { StorageFactory } from '../../storage/services/storage-factory.js'; import { TaskEntity } from '../entities/task.entity.js'; -import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; -import { getLogger } from '../logger/factory.js'; +import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js'; +import { getLogger } from '../../../common/logger/factory.js'; /** * Result returned by getTaskList @@ -483,14 +483,12 @@ export class TaskService { } /** - * Get current storage type + * Get current storage type (resolved at runtime) + * Returns the actual storage type being used, never 'auto' */ - getStorageType(): StorageType { - // Prefer the runtime storage type if available to avoid exposing 'auto' - const s = this.storage as { getType?: () => 'file' | 'api' } | null; - const runtimeType = s?.getType?.(); - return (runtimeType ?? - this.configManager.getStorageConfig().type) as StorageType; + getStorageType(): 'file' | 'api' { + // Storage interface guarantees this method exists + return this.storage.getStorageType(); } /** diff --git a/packages/tm-core/src/modules/tasks/tasks-domain.ts b/packages/tm-core/src/modules/tasks/tasks-domain.ts new file mode 100644 index 00000000..78b03b90 --- /dev/null +++ b/packages/tm-core/src/modules/tasks/tasks-domain.ts @@ -0,0 +1,219 @@ +/** + * @fileoverview Tasks Domain Facade + * Public API for task-related operations + */ + +import type { ConfigManager } from '../config/managers/config-manager.js'; +import { TaskService } from './services/task-service.js'; +import { TaskExecutionService } from './services/task-execution-service.js'; +import { TaskLoaderService } from './services/task-loader.service.js'; +import { PreflightChecker } from './services/preflight-checker.service.js'; + +import type { Task, TaskStatus } from '../../common/types/index.js'; +import type { + TaskListResult, + GetTaskListOptions +} from './services/task-service.js'; +import type { + StartTaskOptions, + StartTaskResult +} from './services/task-execution-service.js'; +import type { + PreflightResult +} from './services/preflight-checker.service.js'; +import type { TaskValidationResult } from './services/task-loader.service.js'; + +/** + * Tasks Domain - Unified API for all task operations + */ +export class TasksDomain { + private taskService: TaskService; + private executionService: TaskExecutionService; + private loaderService: TaskLoaderService; + private preflightChecker: PreflightChecker; + + constructor(configManager: ConfigManager) { + this.taskService = new TaskService(configManager); + this.executionService = new TaskExecutionService(this.taskService); + this.loaderService = new TaskLoaderService(this.taskService); + this.preflightChecker = new PreflightChecker(configManager.getProjectRoot()); + } + + async initialize(): Promise { + await this.taskService.initialize(); + } + + // ========== Task Retrieval ========== + + /** + * Get list of tasks with filtering + */ + async list(options?: GetTaskListOptions): Promise { + return this.taskService.getTaskList(options); + } + + /** + * Get a single task by ID + * Automatically handles all ID formats: + * - Simple task IDs (e.g., "1", "HAM-123") + * - Subtask IDs with dot notation (e.g., "1.2", "HAM-123.2") + * + * @returns Task and whether the ID represents a subtask + */ + async get( + taskId: string, + tag?: string + ): Promise<{ task: Task | null; isSubtask: boolean }> { + // Parse ID - check for dot notation (subtask) + const parts = taskId.split('.'); + const parentId = parts[0]; + const subtaskIdPart = parts[1]; + + // Fetch the task + const task = await this.taskService.getTask(parentId, tag); + if (!task) { + return { task: null, isSubtask: false }; + } + + // Handle subtask notation (1.2, HAM-123.2) + if (subtaskIdPart && task.subtasks) { + const subtask = task.subtasks.find( + (st) => String(st.id) === subtaskIdPart + ); + // Return parent task with isSubtask flag + return { task, isSubtask: !!subtask }; + } + + // It's a regular task + return { task, isSubtask: false }; + } + + /** + * Get tasks by status + */ + async getByStatus(status: TaskStatus, tag?: string): Promise { + return this.taskService.getTasksByStatus(status, tag); + } + + /** + * Get task statistics + */ + async getStats(tag?: string) { + return this.taskService.getTaskStats(tag); + } + + /** + * Get next available task to work on + */ + async getNext(tag?: string): Promise { + return this.taskService.getNextTask(tag); + } + + // ========== Task Status Management ========== + + /** + * Update task status + */ + async updateStatus(taskId: string, status: TaskStatus, tag?: string) { + return this.taskService.updateTaskStatus(taskId, status, tag); + } + + /** + * Set active tag + */ + async setActiveTag(tag: string): Promise { + return this.taskService.setActiveTag(tag); + } + + // ========== Task Execution ========== + + /** + * Start working on a task + */ + async start(taskId: string, options?: StartTaskOptions): Promise { + return this.executionService.startTask(taskId, options); + } + + /** + * Check for in-progress conflicts + */ + async checkInProgressConflicts(taskId: string) { + return this.executionService.checkInProgressConflicts(taskId); + } + + /** + * Get next available task (from execution service) + */ + async getNextAvailable(): Promise { + return this.executionService.getNextAvailableTask(); + } + + /** + * Check if a task can be started + */ + async canStart(taskId: string, force?: boolean): Promise { + return this.executionService.canStartTask(taskId, force); + } + + // ========== Task Loading & Validation ========== + + /** + * Load and validate a task for execution + */ + async loadAndValidate(taskId: string): Promise { + return this.loaderService.loadAndValidateTask(taskId); + } + + /** + * Get execution order for subtasks + */ + getExecutionOrder(task: Task) { + return this.loaderService.getExecutionOrder(task); + } + + // ========== Preflight Checks ========== + + /** + * Run all preflight checks + */ + async runPreflightChecks(): Promise { + return this.preflightChecker.runAllChecks(); + } + + /** + * Detect test command + */ + async detectTestCommand() { + return this.preflightChecker.detectTestCommand(); + } + + /** + * Check git working tree + */ + async checkGitWorkingTree() { + return this.preflightChecker.checkGitWorkingTree(); + } + + /** + * Validate required tools + */ + async validateRequiredTools() { + return this.preflightChecker.validateRequiredTools(); + } + + /** + * Detect default git branch + */ + async detectDefaultBranch() { + return this.preflightChecker.detectDefaultBranch(); + } + + // ========== Storage Information ========== + + /** + * Get the resolved storage type (actual type being used at runtime) + */ + getStorageType(): 'file' | 'api' { + return this.taskService.getStorageType(); + } +} diff --git a/packages/tm-core/src/modules/ui/index.ts b/packages/tm-core/src/modules/ui/index.ts new file mode 100644 index 00000000..bda24f2b --- /dev/null +++ b/packages/tm-core/src/modules/ui/index.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview UI domain - Placeholder for future migration + * This module will handle UI components and rendering + * when migrated from scripts/modules/ui.js + */ + +// TODO: Migrate ui.js from scripts/modules/ +// export * from './components/index.js'; diff --git a/packages/tm-core/src/workflow/workflow-state-manager.spec.ts b/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.spec.ts similarity index 100% rename from packages/tm-core/src/workflow/workflow-state-manager.spec.ts rename to packages/tm-core/src/modules/workflow/managers/workflow-state-manager.spec.ts diff --git a/packages/tm-core/src/workflow/workflow-state-manager.ts b/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts similarity index 98% rename from packages/tm-core/src/workflow/workflow-state-manager.ts rename to packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts index 02e13409..2fd9a329 100644 --- a/packages/tm-core/src/workflow/workflow-state-manager.ts +++ b/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts @@ -9,8 +9,8 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import type { WorkflowState } from './types.js'; -import { getLogger } from '../logger/index.js'; +import type { WorkflowState } from '../types.js'; +import { getLogger } from '../../../common/logger/index.js'; export interface WorkflowStateBackup { timestamp: string; diff --git a/packages/tm-core/src/workflow/workflow-orchestrator.test.ts b/packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.test.ts similarity index 99% rename from packages/tm-core/src/workflow/workflow-orchestrator.test.ts rename to packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.test.ts index 921d43b0..ced1abd5 100644 --- a/packages/tm-core/src/workflow/workflow-orchestrator.test.ts +++ b/packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { WorkflowOrchestrator } from './workflow-orchestrator.js'; +import { WorkflowOrchestrator } from '../orchestrators/workflow-orchestrator.js'; import type { WorkflowContext, WorkflowPhase, WorkflowEventData, WorkflowError -} from './types.js'; +} from '../types.js'; import { TestResultValidator } from '../services/test-result-validator.js'; import type { TestResult } from '../services/test-result-validator.types.js'; diff --git a/packages/tm-core/src/workflow/workflow-orchestrator.ts b/packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.ts similarity index 99% rename from packages/tm-core/src/workflow/workflow-orchestrator.ts rename to packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.ts index e4ac3e1f..20523c86 100644 --- a/packages/tm-core/src/workflow/workflow-orchestrator.ts +++ b/packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.ts @@ -8,8 +8,9 @@ import type { WorkflowEventType, WorkflowEventData, WorkflowEventListener, - SubtaskInfo -} from './types.js'; + SubtaskInfo, + WorkflowError +} from '../types.js'; import type { TestResultValidator } from '../services/test-result-validator.js'; /** @@ -564,7 +565,7 @@ export class WorkflowOrchestrator { /** * Handle error event */ - private handleError(error: import('./types.js').WorkflowError): void { + private handleError(error: WorkflowError): void { this.context.errors.push(error); this.emit('error:occurred', { error }); } diff --git a/packages/tm-core/src/services/test-result-validator.test.ts b/packages/tm-core/src/modules/workflow/services/test-result-validator.test.ts similarity index 100% rename from packages/tm-core/src/services/test-result-validator.test.ts rename to packages/tm-core/src/modules/workflow/services/test-result-validator.test.ts diff --git a/packages/tm-core/src/services/test-result-validator.ts b/packages/tm-core/src/modules/workflow/services/test-result-validator.ts similarity index 100% rename from packages/tm-core/src/services/test-result-validator.ts rename to packages/tm-core/src/modules/workflow/services/test-result-validator.ts diff --git a/packages/tm-core/src/services/test-result-validator.types.ts b/packages/tm-core/src/modules/workflow/services/test-result-validator.types.ts similarity index 100% rename from packages/tm-core/src/services/test-result-validator.types.ts rename to packages/tm-core/src/modules/workflow/services/test-result-validator.types.ts diff --git a/packages/tm-core/src/workflow/workflow-activity-logger.ts b/packages/tm-core/src/modules/workflow/services/workflow-activity-logger.ts similarity index 92% rename from packages/tm-core/src/workflow/workflow-activity-logger.ts rename to packages/tm-core/src/modules/workflow/services/workflow-activity-logger.ts index 43a8bdf3..711be203 100644 --- a/packages/tm-core/src/workflow/workflow-activity-logger.ts +++ b/packages/tm-core/src/modules/workflow/services/workflow-activity-logger.ts @@ -5,10 +5,13 @@ * for debugging, auditing, and workflow analysis. */ -import type { WorkflowOrchestrator } from './workflow-orchestrator.js'; -import type { WorkflowEventData, WorkflowEventType } from './types.js'; -import { logActivity, type ActivityEvent } from '../storage/activity-logger.js'; -import { getLogger } from '../logger/index.js'; +import type { WorkflowOrchestrator } from '../orchestrators/workflow-orchestrator.js'; +import type { WorkflowEventData, WorkflowEventType } from '../types.js'; +import { + logActivity, + type ActivityEvent +} from '../../storage/adapters/activity-logger.js'; +import { getLogger } from '../../../common/logger/index.js'; /** * All workflow event types that should be logged diff --git a/packages/tm-core/src/services/workflow.service.ts b/packages/tm-core/src/modules/workflow/services/workflow.service.ts similarity index 97% rename from packages/tm-core/src/services/workflow.service.ts rename to packages/tm-core/src/modules/workflow/services/workflow.service.ts index 6148273b..21727379 100644 --- a/packages/tm-core/src/services/workflow.service.ts +++ b/packages/tm-core/src/modules/workflow/services/workflow.service.ts @@ -3,9 +3,9 @@ * Provides a simplified API for MCP tools while delegating to WorkflowOrchestrator */ -import { WorkflowOrchestrator } from '../workflow/workflow-orchestrator.js'; -import { WorkflowStateManager } from '../workflow/workflow-state-manager.js'; -import { WorkflowActivityLogger } from '../workflow/workflow-activity-logger.js'; +import { WorkflowOrchestrator } from '../orchestrators/workflow-orchestrator.js'; +import { WorkflowStateManager } from '../managers/workflow-state-manager.js'; +import { WorkflowActivityLogger } from './workflow-activity-logger.js'; import type { WorkflowContext, SubtaskInfo, @@ -13,8 +13,8 @@ import type { WorkflowPhase, TDDPhase, WorkflowState -} from '../workflow/types.js'; -import { GitAdapter } from '../git/git-adapter.js'; +} from '../types.js'; +import { GitAdapter } from '../../git/adapters/git-adapter.js'; /** * Options for starting a new workflow diff --git a/packages/tm-core/src/workflow/types.ts b/packages/tm-core/src/modules/workflow/types.ts similarity index 100% rename from packages/tm-core/src/workflow/types.ts rename to packages/tm-core/src/modules/workflow/types.ts diff --git a/packages/tm-core/src/modules/workflow/workflow-domain.ts b/packages/tm-core/src/modules/workflow/workflow-domain.ts new file mode 100644 index 00000000..f2823321 --- /dev/null +++ b/packages/tm-core/src/modules/workflow/workflow-domain.ts @@ -0,0 +1,98 @@ +/** + * @fileoverview Workflow Domain Facade + * Public API for TDD workflow operations + */ + +import type { ConfigManager } from '../config/managers/config-manager.js'; +import { WorkflowService } from './services/workflow.service.js'; +import type { + StartWorkflowOptions, + WorkflowStatus, + NextAction +} from './services/workflow.service.js'; +import type { TestResult, WorkflowContext } from './types.js'; + +/** + * Workflow Domain - Unified API for TDD workflow operations + */ +export class WorkflowDomain { + private workflowService: WorkflowService; + + constructor(configManager: ConfigManager) { + this.workflowService = new WorkflowService(configManager.getProjectRoot()); + } + + // ========== Workflow Lifecycle ========== + + /** + * Start a new TDD workflow for a task + */ + async start(options: StartWorkflowOptions): Promise { + return this.workflowService.startWorkflow(options); + } + + /** + * Resume an existing workflow + */ + async resume(): Promise { + return this.workflowService.resumeWorkflow(); + } + + /** + * Get current workflow status + */ + getStatus(): WorkflowStatus { + return this.workflowService.getStatus(); + } + + /** + * Get workflow context + */ + getContext(): WorkflowContext { + return this.workflowService.getContext(); + } + + /** + * Get next action to perform in workflow + */ + getNextAction(): NextAction { + return this.workflowService.getNextAction(); + } + + /** + * Complete current phase with test results + */ + async completePhase(testResults: TestResult): Promise { + return this.workflowService.completePhase(testResults); + } + + /** + * Commit changes with auto-generated message + */ + async commit(): Promise { + return this.workflowService.commit(); + } + + /** + * Finalize and complete the workflow + */ + async finalize(): Promise { + return this.workflowService.finalizeWorkflow(); + } + + /** + * Abort the current workflow + */ + async abort(): Promise { + return this.workflowService.abortWorkflow(); + } + + // ========== Workflow Information ========== + + /** + * Check if a workflow currently exists + */ + async hasWorkflow(): Promise { + return this.workflowService.hasWorkflow(); + } +} diff --git a/packages/tm-core/src/services/index.ts b/packages/tm-core/src/services/index.ts deleted file mode 100644 index 92ac4099..00000000 --- a/packages/tm-core/src/services/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Services module exports - * Provides business logic and service layer functionality - */ - -export { TaskService } from './task-service.js'; -export { OrganizationService } from './organization.service.js'; -export { ExportService } from './export.service.js'; -export { PreflightChecker } from './preflight-checker.service.js'; -export { TaskLoaderService } from './task-loader.service.js'; -export { TestResultValidator } from './test-result-validator.js'; -export type { Organization, Brief } from './organization.service.js'; -export type { - ExportTasksOptions, - ExportResult -} from './export.service.js'; -export type { - CheckResult, - PreflightResult -} from './preflight-checker.service.js'; -export type { - TaskValidationResult, - ValidationErrorType, - DependencyIssue -} from './task-loader.service.js'; -export type { - TestResult, - TestPhase, - Coverage, - CoverageThresholds, - ValidationResult, - PhaseValidationOptions -} from './test-result-validator.types.js'; diff --git a/packages/tm-core/src/task-master-core.ts b/packages/tm-core/src/task-master-core.ts deleted file mode 100644 index 6c958825..00000000 --- a/packages/tm-core/src/task-master-core.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * @fileoverview TaskMasterCore facade - main entry point for tm-core functionality - */ - -import { ConfigManager } from './config/config-manager.js'; -import { - TaskService, - type TaskListResult as ListTasksResult, - type GetTaskListOptions -} from './services/task-service.js'; -import { - TaskExecutionService, - type StartTaskOptions, - type StartTaskResult, - type ConflictCheckResult -} from './services/task-execution-service.js'; -import { - ExportService, - type ExportTasksOptions, - type ExportResult -} from './services/export.service.js'; -import { AuthManager } from './auth/auth-manager.js'; -import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js'; -import type { UserContext } from './auth/types.js'; -import type { IConfiguration } from './interfaces/configuration.interface.js'; -import type { - Task, - TaskStatus, - TaskFilter, - StorageType -} from './types/index.js'; -import { - ExecutorService, - type ExecutorServiceOptions, - type ExecutionResult, - type ExecutorType -} from './executors/index.js'; - -/** - * Options for creating TaskMasterCore instance - */ -export interface TaskMasterCoreOptions { - projectPath: string; - configuration?: Partial; -} - -/** - * Re-export result types from services - */ -export type { TaskListResult as ListTasksResult } from './services/task-service.js'; -export type { GetTaskListOptions } from './services/task-service.js'; -export type { - StartTaskOptions, - StartTaskResult, - ConflictCheckResult -} from './services/task-execution-service.js'; -export type { - ExportTasksOptions, - ExportResult -} from './services/export.service.js'; - -/** - * TaskMasterCore facade class - * Provides simplified API for all tm-core operations - */ -export class TaskMasterCore { - private configManager: ConfigManager; - private taskService: TaskService; - private taskExecutionService: TaskExecutionService; - private exportService: ExportService; - private executorService: ExecutorService | null = null; - - /** - * Create and initialize a new TaskMasterCore instance - * This is the ONLY way to create a TaskMasterCore - * - * @param options - Configuration options for TaskMasterCore - * @returns Fully initialized TaskMasterCore instance - */ - static async create(options: TaskMasterCoreOptions): Promise { - const instance = new TaskMasterCore(); - await instance.initialize(options); - return instance; - } - - /** - * Private constructor - use TaskMasterCore.create() instead - * This ensures the TaskMasterCore is always properly initialized - */ - private constructor() { - // Services will be initialized in the initialize() method - this.configManager = null as any; - this.taskService = null as any; - this.taskExecutionService = null as any; - this.exportService = null as any; - } - - /** - * Initialize by loading services - * Private - only called by the factory method - */ - private async initialize(options: TaskMasterCoreOptions): Promise { - if (!options.projectPath) { - throw new TaskMasterError( - 'Project path is required', - ERROR_CODES.MISSING_CONFIGURATION - ); - } - - try { - // Create config manager using factory method - this.configManager = await ConfigManager.create(options.projectPath); - - // Apply configuration overrides if provided - if (options.configuration) { - await this.configManager.updateConfig(options.configuration); - } - - // Create task service - this.taskService = new TaskService(this.configManager); - await this.taskService.initialize(); - - // Create task execution service - this.taskExecutionService = new TaskExecutionService(this.taskService); - - // Create export service - const authManager = AuthManager.getInstance(); - this.exportService = new ExportService(this.configManager, authManager); - } catch (error) { - throw new TaskMasterError( - 'Failed to initialize TaskMasterCore', - ERROR_CODES.INTERNAL_ERROR, - { operation: 'initialize' }, - error as Error - ); - } - } - - /** - * Get list of tasks with optional filtering - * @deprecated Use getTaskList() instead - */ - async listTasks(options?: { - tag?: string; - filter?: TaskFilter; - includeSubtasks?: boolean; - }): Promise { - return this.getTaskList(options); - } - - /** - * Get list of tasks with optional filtering - */ - async getTaskList(options?: GetTaskListOptions): Promise { - return this.taskService.getTaskList(options); - } - - /** - * Get a specific task by ID - */ - async getTask(taskId: string, tag?: string): Promise { - return this.taskService.getTask(taskId, tag); - } - - /** - * Get tasks by status - */ - async getTasksByStatus( - status: TaskStatus | TaskStatus[], - tag?: string - ): Promise { - return this.taskService.getTasksByStatus(status, tag); - } - - /** - * Get task statistics - */ - async getTaskStats(tag?: string): Promise<{ - total: number; - byStatus: Record; - withSubtasks: number; - blocked: number; - }> { - const stats = await this.taskService.getTaskStats(tag); - // Remove storageType from the return to maintain backward compatibility - const { storageType, ...restStats } = stats; - return restStats; - } - - /** - * Get next available task - */ - async getNextTask(tag?: string): Promise { - return this.taskService.getNextTask(tag); - } - - /** - * Get current storage type - */ - getStorageType(): StorageType { - return this.taskService.getStorageType(); - } - - /** - * Get storage configuration - */ - getStorageConfig() { - return this.configManager.getStorageConfig(); - } - - /** - * Get storage display information for headers - * Returns context info for API storage, null for file storage - */ - getStorageDisplayInfo(): { - briefId: string; - briefName: string; - orgSlug?: string; - } | null { - // Only return info if using API storage - const storageType = this.getStorageType(); - if (storageType !== 'api') { - return null; - } - - // Get credentials from auth manager - const authManager = AuthManager.getInstance(); - const credentials = authManager.getCredentials(); - const selectedContext = credentials?.selectedContext; - - if (!selectedContext?.briefId || !selectedContext?.briefName) { - return null; - } - - return { - briefId: selectedContext.briefId, - briefName: selectedContext.briefName, - orgSlug: selectedContext.orgSlug - }; - } - - /** - * Get current active tag - */ - getActiveTag(): string { - return this.configManager.getActiveTag(); - } - - /** - * Set active tag - */ - async setActiveTag(tag: string): Promise { - await this.configManager.setActiveTag(tag); - } - - // ==================== Task Execution Methods ==================== - - /** - * Start working on a task with comprehensive business logic - */ - async startTask( - taskId: string, - options: StartTaskOptions = {} - ): Promise { - return this.taskExecutionService.startTask(taskId, options); - } - - /** - * Check if a task can be started (no conflicts) - */ - async canStartTask(taskId: string, force = false): Promise { - return this.taskExecutionService.canStartTask(taskId, force); - } - - /** - * Check for existing in-progress tasks and determine conflicts - */ - async checkInProgressConflicts( - targetTaskId: string - ): Promise { - return this.taskExecutionService.checkInProgressConflicts(targetTaskId); - } - - /** - * Get task with subtask resolution - */ - async getTaskWithSubtask( - taskId: string - ): Promise<{ task: Task | null; subtask?: any; subtaskId?: string }> { - return this.taskExecutionService.getTaskWithSubtask(taskId); - } - - /** - * Get the next available task to start - */ - async getNextAvailableTask(): Promise { - return this.taskExecutionService.getNextAvailableTask(); - } - - // ==================== Export Service Methods ==================== - - /** - * Export tasks to an external system (e.g., Hamster brief) - */ - async exportTasks(options: ExportTasksOptions): Promise { - return this.exportService.exportTasks(options); - } - - /** - * Export tasks from a brief ID or URL - */ - async exportFromBriefInput(briefInput: string): Promise { - return this.exportService.exportFromBriefInput(briefInput); - } - - /** - * Validate export context before prompting - */ - async validateExportContext(): Promise<{ - hasOrg: boolean; - hasBrief: boolean; - context: UserContext | null; - }> { - return this.exportService.validateContext(); - } - - // ==================== Executor Service Methods ==================== - - /** - * Initialize executor service (lazy initialization) - */ - private getExecutorService(): ExecutorService { - if (!this.executorService) { - const executorOptions: ExecutorServiceOptions = { - projectRoot: this.configManager.getProjectRoot() - }; - this.executorService = new ExecutorService(executorOptions); - } - return this.executorService; - } - - /** - * Execute a task - */ - async executeTask( - task: Task, - executorType?: ExecutorType - ): Promise { - const executor = this.getExecutorService(); - return executor.executeTask(task, executorType); - } - - /** - * Stop the current task execution - */ - async stopCurrentTask(): Promise { - if (this.executorService) { - await this.executorService.stopCurrentTask(); - } - } - - /** - * Update task status - */ - async updateTaskStatus( - taskId: string | number, - newStatus: TaskStatus, - tag?: string - ): Promise<{ - success: boolean; - oldStatus: TaskStatus; - newStatus: TaskStatus; - taskId: string; - }> { - return this.taskService.updateTaskStatus(taskId, newStatus, tag); - } - - /** - * Close and cleanup resources - */ - async close(): Promise { - // Stop any running executors - if (this.executorService) { - await this.executorService.stopCurrentTask(); - } - // TaskService handles storage cleanup internally - } -} - -/** - * Factory function to create TaskMasterCore instance - */ -export async function createTaskMasterCore( - options: TaskMasterCoreOptions -): Promise { - return TaskMasterCore.create(options); -} diff --git a/packages/tm-core/src/tm-core.ts b/packages/tm-core/src/tm-core.ts new file mode 100644 index 00000000..a63de651 --- /dev/null +++ b/packages/tm-core/src/tm-core.ts @@ -0,0 +1,172 @@ +/** + * @fileoverview TmCore - Unified facade for all Task Master functionality + * This is the ONLY entry point for using tm-core + */ + +import path from 'node:path'; +import { ConfigManager } from './modules/config/managers/config-manager.js'; +import { TasksDomain } from './modules/tasks/tasks-domain.js'; +import { AuthDomain } from './modules/auth/auth-domain.js'; +import { WorkflowDomain } from './modules/workflow/workflow-domain.js'; +import { GitDomain } from './modules/git/git-domain.js'; +import { ConfigDomain } from './modules/config/config-domain.js'; +import { IntegrationDomain } from './modules/integration/integration-domain.js'; + +import { + ERROR_CODES, + TaskMasterError +} from './common/errors/task-master-error.js'; +import type { IConfiguration } from './common/interfaces/configuration.interface.js'; + +/** + * Options for creating TmCore instance + */ +export interface TmCoreOptions { + /** Absolute path to project root */ + projectPath: string; + /** Optional configuration overrides */ + configuration?: Partial; +} + +/** + * TmCore - Unified facade providing access to all Task Master domains + * + * @example + * ```typescript + * const tmcore = await createTmCore({ projectPath: process.cwd() }); + * + * // Access any domain + * await tmcore.auth.authenticateWithOAuth(); + * const tasks = await tmcore.tasks.list(); + * await tmcore.workflow.start({ taskId: '1' }); + * await tmcore.git.commit('feat: add feature'); + * const modelConfig = tmcore.config.getModelConfig(); + * await tmcore.integration.exportTasks({ ... }); + * ``` + */ +export class TmCore { + // Core infrastructure + private readonly _projectPath: string; + private _configManager!: ConfigManager; + + // Private writable properties + private _tasks!: TasksDomain; + private _auth!: AuthDomain; + private _workflow!: WorkflowDomain; + private _git!: GitDomain; + private _config!: ConfigDomain; + private _integration!: IntegrationDomain; + + // Public readonly getters + get tasks(): TasksDomain { + return this._tasks; + } + get auth(): AuthDomain { + return this._auth; + } + get workflow(): WorkflowDomain { + return this._workflow; + } + get git(): GitDomain { + return this._git; + } + get config(): ConfigDomain { + return this._config; + } + get integration(): IntegrationDomain { + return this._integration; + } + + /** + * Create and initialize a new TmCore instance + * This is the ONLY way to create TmCore + * + * @param options - Configuration options + * @returns Fully initialized TmCore instance + */ + static async create(options: TmCoreOptions): Promise { + const instance = new TmCore(options); + await instance.initialize(); + return instance; + } + + private _options: TmCoreOptions; + + /** + * Private constructor - use TmCore.create() instead + * This ensures TmCore is always properly initialized + */ + private constructor(options: TmCoreOptions) { + if (!options.projectPath) { + throw new TaskMasterError( + 'Project path is required', + ERROR_CODES.MISSING_CONFIGURATION + ); + } + + // Validate that projectPath is absolute + if (!path.isAbsolute(options.projectPath)) { + throw new TaskMasterError( + `Project path must be an absolute path, received: "${options.projectPath}"`, + ERROR_CODES.INVALID_INPUT + ); + } + + // Normalize the path + this._projectPath = path.resolve(options.projectPath); + this._options = options; + // Domain facades will be initialized in initialize() + } + + /** + * Initialize the TmCore instance + * Private - only called by the factory method + */ + private async initialize(): Promise { + try { + // Create config manager + this._configManager = await ConfigManager.create(this._projectPath); + + // Apply configuration overrides if provided + if (this._options.configuration) { + await this._configManager.updateConfig(this._options.configuration); + } + + // Initialize domain facades + this._tasks = new TasksDomain(this._configManager); + this._auth = new AuthDomain(); + this._workflow = new WorkflowDomain(this._configManager); + this._git = new GitDomain(this._projectPath); + this._config = new ConfigDomain(this._configManager); + this._integration = new IntegrationDomain(this._configManager); + + // Initialize domains that need async setup + await this._tasks.initialize(); + } catch (error) { + throw new TaskMasterError( + 'Failed to initialize TmCore', + ERROR_CODES.INTERNAL_ERROR, + { operation: 'initialize' }, + error as Error + ); + } + } + + /** + * Get project root path + */ + get projectPath(): string { + return this._projectPath; + } +} + +/** + * Factory function to create a new TmCore instance + * This is the recommended way to create TmCore + * + * @param options - Configuration options + * @returns Fully initialized TmCore instance + */ +export async function createTmCore(options: TmCoreOptions): Promise { + return TmCore.create(options); +} diff --git a/packages/tm-core/tests/integration/auth-token-refresh.test.ts b/packages/tm-core/tests/integration/auth-token-refresh.test.ts index 869a0754..10ff45a2 100644 --- a/packages/tm-core/tests/integration/auth-token-refresh.test.ts +++ b/packages/tm-core/tests/integration/auth-token-refresh.test.ts @@ -10,9 +10,9 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import type { Session } from '@supabase/supabase-js'; -import { AuthManager } from '../../src/auth/auth-manager'; -import { CredentialStore } from '../../src/auth/credential-store'; -import type { AuthCredentials } from '../../src/auth/types'; +import { AuthManager } from '../../src/modules/auth/managers/auth-manager.js'; +import { CredentialStore } from '../../src/modules/auth/services/credential-store.js'; +import type { AuthCredentials } from '../../src/modules/auth/types.js'; describe('AuthManager - Token Auto-Refresh Integration', () => { let authManager: AuthManager; diff --git a/update-task-migration-plan.md b/update-task-migration-plan.md new file mode 100644 index 00000000..f9857247 --- /dev/null +++ b/update-task-migration-plan.md @@ -0,0 +1,1406 @@ +# Update Task Migration Plan + +## Overview + +Migrate and unify `update-tasks.js` and `update-subtask-by-id.js` into a single `update-task` command that handles both task and subtask updates. This migration will move from the legacy `scripts/modules/task-manager/` structure to the new `apps/cli` and `packages/tm-core` architecture. + +## Current State Analysis + +### `update-tasks.js` - Bulk Task Updates + +**Purpose**: Update multiple tasks from a specified ID onwards +**Input Format**: `--from= --prompt="context"` +**AI Service**: `generateObjectService` with structured schema + +### `update-subtask-by-id.js` - Single Subtask Updates + +**Purpose**: Append timestamped information to a specific subtask +**Input Format**: `--id= --prompt="notes"` +**AI Service**: `generateTextService` for freeform content + +## Unified Command Design + +### New Command: `update-task` + +```bash +# Update single task (replaces update-task) +task-master update-task --id=3 --prompt="changes" + +# Update single subtask (replaces update-subtask) +task-master update-task --id=3.2 --prompt="implementation notes" + +# Update multiple tasks from ID onwards (replaces update --from) +task-master update-task --from=3 --prompt="changes" +``` + +### Intelligent Behavior Detection + +The command should automatically determine behavior based on: + +1. **ID format**: Contains `.` β†’ subtask mode +2. **--from flag**: Present β†’ bulk update mode +3. **Default**: Single task update mode + +--- + +## Functionality Checklist + +### Core Functionality + +#### Input Validation & Parsing + +- [ ] Validate `tasksPath` exists +- [ ] Validate `id` parameter (task: integer, subtask: "parent.child" format) +- [ ] Validate `fromId` parameter (integer, positive) +- [ ] Validate `prompt` parameter (non-empty string) +- [ ] Parse subtask ID format: split "parentId.subtaskId" and validate both parts +- [ ] Determine project root (from context or `findProjectRoot()`) +- [ ] Support both MCP and CLI modes (detect via `mcpLog` presence) +- [ ] Handle `outputFormat` ('text' or 'json', auto-detect for MCP) + +#### Task Loading & Filtering + +- [ ] Load tasks from `tasks.json` using `readJSON(tasksPath, projectRoot, tag)` +- [ ] Validate tasks data structure exists +- [ ] **Bulk mode**: Filter tasks where `id >= fromId AND status !== 'done'` +- [ ] **Single task mode**: Find specific task by ID +- [ ] **Subtask mode**: Find parent task, validate subtasks array, find specific subtask +- [ ] Handle "no tasks to update" scenario gracefully + +#### Context Gathering + +- [ ] Initialize `ContextGatherer` with projectRoot and tag +- [ ] Flatten all tasks with subtasks using `flattenTasksWithSubtasks()` +- [ ] Initialize `FuzzyTaskSearch` with appropriate command type: + - `'update'` for bulk/single task mode + - `'update-subtask'` for subtask mode +- [ ] **Bulk/Single task**: Search with prompt, max 5 results, include self +- [ ] **Subtask mode**: Search with combined query: `${parentTask.title} ${subtask.title} ${prompt}` +- [ ] Merge task IDs to update with relevant context task IDs +- [ ] Gather context in 'research' format +- [ ] Handle context gathering errors gracefully (log warning, continue) + +#### Prompt Building + +- [ ] Initialize `PromptManager` via `getPromptManager()` +- [ ] **Bulk/Single task mode**: Load 'update-tasks' prompt template with params: + - `tasks` (array of tasks to update) + - `updatePrompt` + - `useResearch` + - `projectContext` (gathered context) + - `hasCodebaseAnalysis` (from config) + - `projectRoot` +- [ ] **Subtask mode**: Load 'update-subtask' prompt template with params: + - `parentTask` (id, title) + - `prevSubtask` (id, title, status) - if exists + - `nextSubtask` (id, title, status) - if exists + - `currentDetails` (existing subtask details or fallback) + - `updatePrompt` + - `useResearch` + - `gatheredContext` + - `hasCodebaseAnalysis` + - `projectRoot` +- [ ] **Subtask mode**: Support variant key ('research' or 'default') +- [ ] Extract `systemPrompt` and `userPrompt` from prompt manager + +#### AI Service Integration + +- [ ] Determine service role: `useResearch ? 'research' : 'main'` +- [ ] **Bulk/Single task mode**: Call `generateObjectService` with: + - `role`, `session`, `projectRoot` + - `systemPrompt`, `prompt` (userPrompt) + - `schema: COMMAND_SCHEMAS['update-tasks']` + - `objectName: 'tasks'` + - `commandName: 'update-tasks'` + - `outputType: isMCP ? 'mcp' : 'cli'` +- [ ] **Subtask mode**: Call `generateTextService` with: + - `prompt` (userPrompt), `systemPrompt` + - `role`, `session`, `projectRoot` + - `maxRetries: 2` + - `commandName: 'update-subtask'` + - `outputType: isMCP ? 'mcp' : 'cli'` +- [ ] Handle empty/invalid AI responses +- [ ] Capture `telemetryData` and `tagInfo` from response + +#### Data Updates & Persistence + +- [ ] **Bulk/Single task mode**: + - Parse `aiServiceResponse.mainResult.tasks` array + - Validate array structure + - Create Map for efficient lookup + - Merge updated tasks with existing, preserving subtasks field + - Track actual update count +- [ ] **Subtask mode**: + - Extract text string from `aiServiceResponse.mainResult` + - Generate ISO timestamp + - Format as: `\n${content}\n` + - Append to `subtask.details` (create if doesn't exist) + - Store newly added snippet separately for display + - If prompt < 100 chars: append `[Updated: ${date}]` to subtask.description +- [ ] Write updated data using `writeJSON(tasksPath, data, projectRoot, tag)` +- [ ] Optionally call `generateTaskFiles()` (currently commented out in both) + +#### CLI Display & UX + +- [ ] **Pre-update display** (CLI only, text mode): + - Create table with columns: ID, Title, Status + - Truncate titles appropriately (57 chars for tasks, 52 for subtasks) + - Apply status colors via `getStatusWithColor()` + - Show boxed header with update count/target + - **Bulk mode**: Show info box about completed subtasks handling + - Display table +- [ ] **Loading indicators** (CLI only, text mode): + - Start loading indicator before AI call + - Message: "Updating tasks with AI..." (bulk/single) or "Updating subtask..." (subtask) + - Support research variant message + - Stop indicator when complete or on error +- [ ] **Post-update display** (CLI only, text mode): + - **Bulk/Single task**: Success message with update count + - **Subtask mode**: Boxed success message with: + - Subtask ID + - Title + - "Newly Added Snippet" section showing timestamped content + - Display AI usage summary via `displayAiUsageSummary(telemetryData, 'cli')` + +#### Logging & Debugging + +- [ ] Use appropriate logger: `mcpLog` (MCP) or `consoleLog` (CLI) +- [ ] Log info messages with proper format (MCP vs CLI differences) +- [ ] Log start of operation with key parameters +- [ ] Log task counts and AI response details +- [ ] Log successful completion +- [ ] **Debug mode** (when `getDebugFlag(session)` true): + - Log subtask details before/after update + - Log writeJSON calls + - Log full error stack traces + +#### Error Handling + +- [ ] Catch and handle errors at multiple levels: + - Context gathering errors (warn and continue) + - AI service errors (stop and report) + - General operation errors (report and exit/throw) +- [ ] **CLI mode**: + - Print colored error messages + - Show helpful troubleshooting for common errors: + - API key missing/invalid + - Model overloaded + - Task/subtask not found + - Invalid ID format + - Empty prompt + - Empty AI response + - Exit with code 1 +- [ ] **MCP mode**: Re-throw errors for caller handling +- [ ] Always stop loading indicators on error + +#### Return Values + +- [ ] **Success returns** (both modes): + + ```javascript + { + success: true, // bulk/single task only + updatedTasks: [...], // bulk/single task only + updatedSubtask: {...}, // subtask only + telemetryData: {...}, + tagInfo: {...} + } + ``` + +- [ ] **Failure returns**: + - CLI: exits with code 1 + - MCP: throws error + - Subtask mode: returns `null` on error + +### Special Features + +#### Completed Subtasks Handling (Bulk Mode) + +- [ ] Display informational box explaining: + - Done/completed subtasks are preserved + - New subtasks build upon completed work + - Revisions create new subtasks instead of modifying done items + - Maintains clear record of progress + +#### Subtask Context Awareness + +- [ ] Provide parent task context (id, title) to AI +- [ ] Provide previous subtask context (if exists) to AI +- [ ] Provide next subtask context (if exists) to AI +- [ ] Include current subtask details in prompt + +#### Timestamp Tracking + +- [ ] Use ISO format timestamps for subtask updates +- [ ] Wrap appended content in timestamped tags +- [ ] Update description field with simple date stamp (short prompts only) + +--- + +## Migration Architecture + +### Object-Oriented Design Philosophy + +This migration will follow the established patterns in `tm-core` and `apps/cli`: +- **Domain separation** with clear bounded contexts +- **Dependency injection** for testability and flexibility +- **Abstract base classes** for shared behavior +- **Interfaces** for contracts and loose coupling +- **Service layer** for business logic orchestration +- **Factory pattern** for object creation +- **Single Responsibility Principle** throughout + +### Package Structure + +``` +packages/tm-core/ + src/ + commands/ + update-task/ + # Core Interfaces & Types + types.ts # Shared types, enums, interfaces + interfaces/ + update-strategy.interface.ts # IUpdateStrategy contract + update-context.interface.ts # IUpdateContext contract + display.interface.ts # IDisplayManager contract + + # Services (Business Logic) + update-task.service.ts # Main orchestrator service + context-builder.service.ts # Builds AI context (uses ContextGatherer, FuzzySearch) + prompt-builder.service.ts # Builds prompts (uses PromptManager) + data-merger.service.ts # Merges AI results with existing data + + # Strategies (Update Mode Logic) + strategies/ + base-update.strategy.ts # Abstract base class for all strategies + bulk-update.strategy.ts # Bulk task update implementation + single-task-update.strategy.ts # Single task update implementation + subtask-update.strategy.ts # Subtask update implementation + + # Utilities & Helpers + validators/ + update-input.validator.ts # Validates all input parameters + task-id.validator.ts # Parses and validates task/subtask IDs + + display/ + cli-display.manager.ts # CLI output formatting + json-display.manager.ts # JSON output formatting + update-display.factory.ts # Creates appropriate display manager + + factories/ + update-strategy.factory.ts # Creates appropriate strategy based on mode + + # Main Entry Point + index.ts # Public API export + +apps/cli/ + src/ + commands/ + update-task.command.ts # CLI command definition (uses UpdateTaskService) +``` + +### Core Classes & Their Responsibilities + +#### 1. **UpdateTaskService** (Main Orchestrator) +```typescript +/** + * Main service that coordinates the entire update process + * Handles initialization, strategy selection, and result aggregation + */ +export class UpdateTaskService { + constructor( + private readonly configManager: ConfigManager, + private readonly storage: IStorage, + private readonly logger: Logger, + private readonly strategyFactory: UpdateStrategyFactory, + private readonly contextBuilder: ContextBuilderService, + private readonly displayFactory: UpdateDisplayFactory + ) {} + + async updateTask(options: UpdateTaskOptions): Promise { + // 1. Validate inputs + // 2. Detect mode and create strategy + // 3. Build context + // 4. Execute strategy + // 5. Display results + // 6. Return result + } +} +``` + +**Uses (existing classes):** +- `ConfigManager` - Project configuration +- `IStorage` - Task persistence +- `Logger` - Logging +- `ContextGatherer` - Gather related context +- `FuzzyTaskSearch` - Find relevant tasks +- `PromptManager` - Load prompt templates + +**Uses (new classes):** +- `UpdateStrategyFactory` - Create update strategy +- `ContextBuilderService` - Build AI context +- `UpdateDisplayFactory` - Create display manager + +--- + +#### 2. **IUpdateStrategy** (Strategy Interface) +```typescript +/** + * Contract for all update strategies + * Defines the common interface for bulk, single, and subtask updates + */ +export interface IUpdateStrategy { + /** + * Validate that the strategy can handle the given context + */ + validate(context: IUpdateContext): Promise; + + /** + * Load and filter tasks that need updating + */ + loadTasks(context: IUpdateContext): Promise; + + /** + * Build prompts for AI service + */ + buildPrompts( + context: IUpdateContext, + tasks: TaskLoadResult + ): Promise; + + /** + * Call appropriate AI service + */ + callAIService( + context: IUpdateContext, + prompts: PromptResult + ): Promise; + + /** + * Merge AI results with existing data + */ + mergeResults( + context: IUpdateContext, + aiResult: AIServiceResult, + originalTasks: TaskLoadResult + ): Promise; + + /** + * Get the mode this strategy handles + */ + getMode(): UpdateMode; +} +``` + +--- + +#### 3. **BaseUpdateStrategy** (Abstract Base Class) +```typescript +/** + * Provides common functionality for all update strategies + * Implements template method pattern for the update workflow + */ +export abstract class BaseUpdateStrategy implements IUpdateStrategy { + protected readonly logger: Logger; + + constructor( + protected readonly contextBuilder: ContextBuilderService, + protected readonly promptBuilder: PromptBuilderService, + protected readonly dataMerger: DataMergerService, + protected readonly aiService: AIService // wrapper around generate[Object|Text]Service + ) { + this.logger = getLogger(`UpdateStrategy:${this.getMode()}`); + } + + // Template method - defines the workflow + async execute(context: IUpdateContext): Promise { + await this.validate(context); + const tasks = await this.loadTasks(context); + const prompts = await this.buildPrompts(context, tasks); + const aiResult = await this.callAIService(context, prompts); + const merged = await this.mergeResults(context, aiResult, tasks); + return merged; + } + + // Subclasses must implement these + abstract validate(context: IUpdateContext): Promise; + abstract loadTasks(context: IUpdateContext): Promise; + abstract getMode(): UpdateMode; + + // Shared implementations with extensibility + async buildPrompts( + context: IUpdateContext, + tasks: TaskLoadResult + ): Promise { + // Delegates to PromptBuilderService with mode-specific params + } + + protected abstract getPromptParams( + context: IUpdateContext, + tasks: TaskLoadResult + ): PromptParams; +} +``` + +--- + +#### 4. **BulkUpdateStrategy** (Concrete Strategy) +```typescript +/** + * Handles bulk task updates (--from flag) + * Uses generateObjectService for structured updates + */ +export class BulkUpdateStrategy extends BaseUpdateStrategy { + getMode(): UpdateMode { + return UpdateMode.BULK; + } + + async validate(context: IUpdateContext): Promise { + if (!context.options.from) { + throw new TaskMasterError('Bulk mode requires --from parameter'); + } + // Additional validations... + } + + async loadTasks(context: IUpdateContext): Promise { + // Filter tasks where id >= fromId AND status !== 'done' + } + + async callAIService( + context: IUpdateContext, + prompts: PromptResult + ): Promise { + // Call generateObjectService with update-tasks schema + } + + protected getPromptParams( + context: IUpdateContext, + tasks: TaskLoadResult + ): PromptParams { + return { + tasks: tasks.tasks, + updatePrompt: context.options.prompt, + useResearch: context.options.useResearch, + projectContext: tasks.gatheredContext, + // ... + }; + } +} +``` + +--- + +#### 5. **SubtaskUpdateStrategy** (Concrete Strategy) +```typescript +/** + * Handles single subtask updates (--id with dot notation) + * Uses generateTextService for timestamped appends + */ +export class SubtaskUpdateStrategy extends BaseUpdateStrategy { + getMode(): UpdateMode { + return UpdateMode.SUBTASK; + } + + async validate(context: IUpdateContext): Promise { + const parsed = TaskIdValidator.parseSubtaskId(context.options.id); + if (!parsed) { + throw new TaskMasterError('Invalid subtask ID format'); + } + } + + async loadTasks(context: IUpdateContext): Promise { + // Find parent task, locate specific subtask + // Build context with prev/next subtask info + } + + async callAIService( + context: IUpdateContext, + prompts: PromptResult + ): Promise { + // Call generateTextService for freeform content + } + + async mergeResults( + context: IUpdateContext, + aiResult: AIServiceResult, + originalTasks: TaskLoadResult + ): Promise { + // Append timestamped content to subtask.details + const timestamp = new Date().toISOString(); + const formatted = `\n${aiResult.text}\n`; + // ... + } +} +``` + +--- + +#### 6. **SingleTaskUpdateStrategy** (Concrete Strategy) +```typescript +/** + * Handles single task updates (--id without dot) + * Uses generateObjectService for structured updates + */ +export class SingleTaskUpdateStrategy extends BaseUpdateStrategy { + getMode(): UpdateMode { + return UpdateMode.SINGLE; + } + + async validate(context: IUpdateContext): Promise { + TaskIdValidator.validateTaskId(context.options.id); + } + + async loadTasks(context: IUpdateContext): Promise { + // Find single task by ID + } + + // Similar to BulkUpdateStrategy but operates on single task +} +``` + +--- + +#### 7. **ContextBuilderService** (Helper Service) +```typescript +/** + * Builds context for AI prompts + * Coordinates ContextGatherer and FuzzyTaskSearch + */ +export class ContextBuilderService { + constructor( + private readonly logger: Logger + ) {} + + async buildContext( + options: ContextBuildOptions + ): Promise { + try { + const gatherer = new ContextGatherer( + options.projectRoot, + options.tag + ); + + const allTasksFlat = flattenTasksWithSubtasks(options.allTasks); + const fuzzySearch = new FuzzyTaskSearch( + allTasksFlat, + options.searchMode // 'update' or 'update-subtask' + ); + + const searchResults = fuzzySearch.findRelevantTasks( + options.searchQuery, + { maxResults: 5, includeSelf: true } + ); + + const relevantTaskIds = fuzzySearch.getTaskIds(searchResults); + const finalTaskIds = [ + ...new Set([...options.targetTaskIds, ...relevantTaskIds]) + ]; + + const contextResult = await gatherer.gather({ + tasks: finalTaskIds, + format: 'research' + }); + + return { + context: contextResult.context || '', + taskIds: finalTaskIds + }; + } catch (error) { + this.logger.warn(`Context gathering failed: ${error.message}`); + return { context: '', taskIds: options.targetTaskIds }; + } + } +} +``` + +**Uses (existing):** +- `ContextGatherer` +- `FuzzyTaskSearch` + +--- + +#### 8. **PromptBuilderService** (Helper Service) +```typescript +/** + * Builds system and user prompts for AI services + * Wraps PromptManager with strategy-specific logic + */ +export class PromptBuilderService { + constructor( + private readonly promptManager: PromptManager, + private readonly logger: Logger + ) {} + + async buildPrompt( + templateName: string, + params: PromptParams, + variant?: string + ): Promise { + const { systemPrompt, userPrompt } = await this.promptManager.loadPrompt( + templateName, + params, + variant + ); + + return { + systemPrompt, + userPrompt, + templateName, + params + }; + } +} +``` + +**Uses (existing):** +- `PromptManager` + +--- + +#### 9. **DataMergerService** (Helper Service) +```typescript +/** + * Merges AI service results with existing task data + * Handles different merge strategies for different modes + */ +export class DataMergerService { + constructor(private readonly logger: Logger) {} + + /** + * Merge for bulk/single task mode (structured updates) + */ + mergeTasks( + existingTasks: Task[], + updatedTasks: Task[], + options: MergeOptions + ): MergeResult { + const updatedTasksMap = new Map( + updatedTasks.map(t => [t.id, t]) + ); + + let updateCount = 0; + const merged = existingTasks.map(task => { + if (updatedTasksMap.has(task.id)) { + const updated = updatedTasksMap.get(task.id)!; + updateCount++; + return { + ...task, + ...updated, + // Preserve subtasks if not provided by AI + subtasks: updated.subtasks !== undefined + ? updated.subtasks + : task.subtasks + }; + } + return task; + }); + + return { + tasks: merged, + updateCount, + mode: 'structured' + }; + } + + /** + * Merge for subtask mode (timestamped append) + */ + mergeSubtask( + parentTask: Task, + subtaskIndex: number, + newContent: string, + options: SubtaskMergeOptions + ): SubtaskMergeResult { + const subtask = parentTask.subtasks![subtaskIndex]; + const timestamp = new Date().toISOString(); + const formatted = `\n${newContent.trim()}\n`; + + subtask.details = (subtask.details ? subtask.details + '\n' : '') + formatted; + + // Short prompts get description timestamp + if (options.prompt.length < 100 && subtask.description) { + subtask.description += ` [Updated: ${new Date().toLocaleDateString()}]`; + } + + return { + updatedSubtask: subtask, + newlyAddedSnippet: formatted, + parentTask + }; + } +} +``` + +--- + +#### 10. **IDisplayManager** (Display Interface) +```typescript +/** + * Contract for display managers + * Allows different output formats (CLI, JSON, etc.) + */ +export interface IDisplayManager { + /** + * Show tasks before update + */ + showPreUpdate(tasks: Task[], mode: UpdateMode): void; + + /** + * Show loading indicator + */ + startLoading(message: string): void; + stopLoading(success?: boolean): void; + + /** + * Show post-update results + */ + showPostUpdate(result: UpdateStrategyResult, mode: UpdateMode): void; + + /** + * Show telemetry/usage data + */ + showTelemetry(telemetry: TelemetryData): void; + + /** + * Show errors + */ + showError(error: Error): void; +} +``` + +--- + +#### 11. **CLIDisplayManager** (Concrete Display) +```typescript +/** + * Formats output for CLI with colors, tables, and boxes + */ +export class CLIDisplayManager implements IDisplayManager { + constructor( + private readonly logger: Logger, + private readonly isSilent: boolean + ) {} + + showPreUpdate(tasks: Task[], mode: UpdateMode): void { + // Create table with ID, Title, Status columns + // Show boxed header + // For bulk mode: show completed subtasks info box + } + + startLoading(message: string): void { + // startLoadingIndicator(message) + } + + // ... implement other methods with chalk, boxen, cli-table3 +} +``` + +--- + +#### 12. **UpdateStrategyFactory** (Factory) +```typescript +/** + * Creates the appropriate update strategy based on mode + */ +export class UpdateStrategyFactory { + constructor( + private readonly contextBuilder: ContextBuilderService, + private readonly promptBuilder: PromptBuilderService, + private readonly dataMerger: DataMergerService, + private readonly aiService: AIService + ) {} + + createStrategy(mode: UpdateMode): IUpdateStrategy { + switch (mode) { + case UpdateMode.BULK: + return new BulkUpdateStrategy( + this.contextBuilder, + this.promptBuilder, + this.dataMerger, + this.aiService + ); + case UpdateMode.SINGLE: + return new SingleTaskUpdateStrategy( + this.contextBuilder, + this.promptBuilder, + this.dataMerger, + this.aiService + ); + case UpdateMode.SUBTASK: + return new SubtaskUpdateStrategy( + this.contextBuilder, + this.promptBuilder, + this.dataMerger, + this.aiService + ); + default: + throw new TaskMasterError(`Unknown update mode: ${mode}`); + } + } + + detectMode(options: UpdateTaskOptions): UpdateMode { + if (options.from !== undefined) { + return UpdateMode.BULK; + } + if (options.id && typeof options.id === 'string' && options.id.includes('.')) { + return UpdateMode.SUBTASK; + } + if (options.id !== undefined) { + return UpdateMode.SINGLE; + } + throw new TaskMasterError('Must provide either --id or --from parameter'); + } +} +``` + +--- + +#### 13. **Validators** (Utility Classes) +```typescript +/** + * Validates all update task inputs + */ +export class UpdateInputValidator { + static validate(options: UpdateTaskOptions): void { + // Validate tasksPath, prompt, etc. + } +} + +/** + * Parses and validates task/subtask IDs + */ +export class TaskIdValidator { + static validateTaskId(id: any): number { + // Parse and validate task ID + } + + static parseSubtaskId(id: string): SubtaskIdParts | null { + // Parse "parentId.subtaskId" format + } +} +``` + +--- + +### Class Diagram (Relationships) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ UpdateTaskService β”‚ ◄─── Main Orchestrator +β”‚ (Coordinates) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ uses + β”œβ”€β”€β–Ί UpdateStrategyFactory ──creates──► IUpdateStrategy + β”‚ β”‚ + β”œβ”€β”€β–Ί ContextBuilderService β”‚ implements + β”‚ β–Ό + β”œβ”€β”€β–Ί IDisplayManager ◄──creates── UpdateDisplayFactory + β”‚ β”‚ + β”‚ β”œβ”€β”€ CLIDisplayManager + β”‚ └── JSONDisplayManager + β”‚ + └──► ConfigManager (existing) + IStorage (existing) + Logger (existing) + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ IUpdateStrategy β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β–³ + β”‚ extends + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ BaseUpdateStrategy β”‚ β”‚ Abstract base with β”‚ +β”‚ (Template Method) β”‚ β”‚ common workflow β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ extends + β”Œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ β”‚ +β”Œβ”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ Bulk β”‚ β”‚Singleβ”‚ β”‚ Subtask β”‚ β”‚ +β”‚Update β”‚ β”‚Task β”‚ β”‚ Update β”‚ β”‚ +β”‚ β”‚ β”‚Updateβ”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ + β”œβ”€β”€β–Ί ContextBuilderService + β”‚ β”œβ”€uses─► ContextGatherer (existing) + β”‚ └─uses─► FuzzyTaskSearch (existing) + β”‚ + β”œβ”€β”€β–Ί PromptBuilderService + β”‚ └─uses─► PromptManager (existing) + β”‚ + └──► DataMergerService +``` + +--- + +### Dependency Injection & Initialization + +```typescript +// In packages/tm-core/src/commands/update-task/index.ts + +/** + * Factory function to create a fully initialized UpdateTaskService + */ +export async function createUpdateTaskService( + configManager: ConfigManager, + storage: IStorage +): Promise { + const logger = getLogger('UpdateTaskService'); + + // Create helper services + const contextBuilder = new ContextBuilderService(logger); + const promptManager = getPromptManager(); // existing + const promptBuilder = new PromptBuilderService(promptManager, logger); + const dataMerger = new DataMergerService(logger); + const aiService = new AIService(); // wrapper around generateObjectService/generateTextService + + // Create factory + const strategyFactory = new UpdateStrategyFactory( + contextBuilder, + promptBuilder, + dataMerger, + aiService + ); + + // Create display factory + const displayFactory = new UpdateDisplayFactory(); + + // Create service + return new UpdateTaskService( + configManager, + storage, + logger, + strategyFactory, + contextBuilder, + displayFactory + ); +} +``` + +--- + +### Types & Interfaces + +```typescript +// packages/tm-core/src/commands/update-task/types.ts + +export enum UpdateMode { + BULK = 'bulk', + SINGLE = 'single', + SUBTASK = 'subtask' +} + +export interface UpdateTaskOptions { + tasksPath: string; + id?: number | string; + from?: number; + prompt: string; + useResearch?: boolean; + context?: UpdateContext; + outputFormat?: 'text' | 'json'; +} + +export interface UpdateContext { + session?: any; + mcpLog?: any; + projectRoot?: string; + tag?: string; +} + +export interface UpdateTaskResult { + success: boolean; + mode: UpdateMode; + updatedTasks?: Task[]; + updatedSubtask?: Subtask; + updateCount?: number; + telemetryData?: TelemetryData; + tagInfo?: TagInfo; +} + +export interface IUpdateContext { + options: UpdateTaskOptions; + projectRoot: string; + tag?: string; + mode: UpdateMode; + isMCP: boolean; + logger: Logger; +} + +export interface TaskLoadResult { + tasks: Task[]; + gatheredContext: string; + originalData: TasksData; +} + +export interface PromptResult { + systemPrompt: string; + userPrompt: string; + templateName: string; + params: PromptParams; +} + +export interface AIServiceResult { + mainResult: any; // structured object or text string + telemetryData?: TelemetryData; + tagInfo?: TagInfo; +} + +export interface MergeResult { + tasks?: Task[]; + updatedSubtask?: Subtask; + newlyAddedSnippet?: string; + updateCount: number; + mode: 'structured' | 'timestamped'; +} +``` + +--- + +## Implementation Phases + +### Phase 1: Foundation & Core Types + +**Goal**: Establish the type system and interfaces + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/types.ts` + - Define `UpdateMode` enum + - Define all shared interfaces (`UpdateTaskOptions`, `UpdateTaskResult`, etc.) +2. `packages/tm-core/src/commands/update-task/interfaces/update-strategy.interface.ts` + - Define `IUpdateStrategy` interface +3. `packages/tm-core/src/commands/update-task/interfaces/update-context.interface.ts` + - Define `IUpdateContext` interface +4. `packages/tm-core/src/commands/update-task/interfaces/display.interface.ts` + - Define `IDisplayManager` interface + +**Existing Classes to Study**: +- `BaseExecutor` - For abstract class patterns +- `TaskService` - For service patterns +- `IStorage` - For interface patterns + +--- + +### Phase 2: Validator & Helper Utilities + +**Goal**: Build validation and utility classes + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/validators/update-input.validator.ts` + - Create `UpdateInputValidator` class + - Port validation logic from both old files +2. `packages/tm-core/src/commands/update-task/validators/task-id.validator.ts` + - Create `TaskIdValidator` class + - Implement `validateTaskId()` and `parseSubtaskId()` methods + +**Tests to Create**: +- `update-input.validator.spec.ts` +- `task-id.validator.spec.ts` + +--- + +### Phase 3: Service Layer + +**Goal**: Build the helper services that strategies will use + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/context-builder.service.ts` + - Create `ContextBuilderService` class + - **Uses existing**: `ContextGatherer`, `FuzzyTaskSearch` + - Port context gathering logic from both old files + +2. `packages/tm-core/src/commands/update-task/prompt-builder.service.ts` + - Create `PromptBuilderService` class + - **Uses existing**: `PromptManager` (via `getPromptManager()`) + - Port prompt building logic + +3. `packages/tm-core/src/commands/update-task/data-merger.service.ts` + - Create `DataMergerService` class + - Implement `mergeTasks()` method (from `update-tasks.js` lines 250-273) + - Implement `mergeSubtask()` method (from `update-subtask-by-id.js` lines 291-332) + +**Tests to Create**: +- `context-builder.service.spec.ts` +- `prompt-builder.service.spec.ts` +- `data-merger.service.spec.ts` + +**Existing Classes Used**: +- `ContextGatherer` (from `scripts/modules/utils/contextGatherer.js`) +- `FuzzyTaskSearch` (from `scripts/modules/utils/fuzzyTaskSearch.js`) +- `PromptManager` (from `scripts/modules/prompt-manager.js`) + +--- + +### Phase 4: Strategy Pattern Implementation + +**Goal**: Implement the update strategies + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/strategies/base-update.strategy.ts` + - Create `BaseUpdateStrategy` abstract class implementing `IUpdateStrategy` + - Implement template method pattern + - Define abstract methods for subclasses + +2. `packages/tm-core/src/commands/update-task/strategies/bulk-update.strategy.ts` + - Create `BulkUpdateStrategy` class extending `BaseUpdateStrategy` + - Port logic from `update-tasks.js` lines 79-293 + - **Uses**: `generateObjectService` with `COMMAND_SCHEMAS['update-tasks']` + +3. `packages/tm-core/src/commands/update-task/strategies/single-task-update.strategy.ts` + - Create `SingleTaskUpdateStrategy` class extending `BaseUpdateStrategy` + - Similar to bulk but for single task + - **Uses**: `generateObjectService` with `COMMAND_SCHEMAS['update-tasks']` + +4. `packages/tm-core/src/commands/update-task/strategies/subtask-update.strategy.ts` + - Create `SubtaskUpdateStrategy` class extending `BaseUpdateStrategy` + - Port logic from `update-subtask-by-id.js` lines 67-378 + - **Uses**: `generateTextService` for freeform content + +**Tests to Create**: +- `bulk-update.strategy.spec.ts` +- `single-task-update.strategy.spec.ts` +- `subtask-update.strategy.spec.ts` + +**Existing Classes/Functions Used**: +- `generateObjectService` (from `scripts/modules/ai-services-unified.js`) +- `generateTextService` (from `scripts/modules/ai-services-unified.js`) +- `COMMAND_SCHEMAS` (from `src/schemas/registry.js`) +- `readJSON`, `writeJSON`, `flattenTasksWithSubtasks` (from `scripts/modules/utils.js`) + +--- + +### Phase 5: Display Layer + +**Goal**: Implement display managers for different output formats + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/display/cli-display.manager.ts` + - Create `CLIDisplayManager` class implementing `IDisplayManager` + - Port CLI display logic from both old files + - **Uses existing**: `chalk`, `boxen`, `cli-table3`, `getStatusWithColor`, `truncate` + +2. `packages/tm-core/src/commands/update-task/display/json-display.manager.ts` + - Create `JSONDisplayManager` class implementing `IDisplayManager` + - Implement JSON output format (for MCP) + +3. `packages/tm-core/src/commands/update-task/display/update-display.factory.ts` + - Create `UpdateDisplayFactory` class + - Factory method to create appropriate display manager + +**Tests to Create**: +- `cli-display.manager.spec.ts` +- `json-display.manager.spec.ts` + +**Existing Functions Used**: +- `getStatusWithColor`, `startLoadingIndicator`, `stopLoadingIndicator`, `displayAiUsageSummary` (from `scripts/modules/ui.js`) +- `truncate`, `isSilentMode` (from `scripts/modules/utils.js`) + +--- + +### Phase 6: Factory Pattern + +**Goal**: Implement factory for creating strategies + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/factories/update-strategy.factory.ts` + - Create `UpdateStrategyFactory` class + - Implement `createStrategy(mode)` method + - Implement `detectMode(options)` method + - Handles dependency injection for all strategies + +**Tests to Create**: +- `update-strategy.factory.spec.ts` (test mode detection and strategy creation) + +--- + +### Phase 7: Main Service Orchestrator + +**Goal**: Create the main service that ties everything together + +**New Files to Create**: +1. `packages/tm-core/src/commands/update-task/update-task.service.ts` + - Create `UpdateTaskService` class + - Main orchestrator that coordinates all components + - Implements high-level workflow + +2. `packages/tm-core/src/commands/update-task/index.ts` + - Export all public types and interfaces + - Export `createUpdateTaskService()` factory function + - Export `UpdateTaskService` class + +**Tests to Create**: +- `update-task.service.spec.ts` (integration tests) + +**Existing Classes Used**: +- `ConfigManager` (from `packages/tm-core/src/config/config-manager.ts`) +- `IStorage` (from `packages/tm-core/src/interfaces/storage.interface.ts`) +- `Logger`, `getLogger` (from `packages/tm-core/src/logger/`) + +--- + +### Phase 8: CLI Integration + +**Goal**: Wire up the new service to the CLI + +**New Files to Create**: +1. `apps/cli/src/commands/update-task.command.ts` + - CLI command definition using `commander` + - Calls `createUpdateTaskService()` and executes + - Handles CLI-specific argument parsing + +**Files to Modify**: +1. `apps/cli/src/index.ts` (or main CLI entry point) + - Register new `update-task` command + - Optionally add aliases for backward compatibility + +**Existing Patterns to Follow**: +- Study existing CLI commands in `apps/cli/src/commands/` +- Follow same pattern for option parsing and service invocation + +--- + +### Phase 9: Integration & Testing + +**Goal**: Ensure everything works together + +**Tasks**: +1. Run full integration tests + - Test bulk update workflow end-to-end + - Test single task update workflow + - Test subtask update workflow + - Test MCP mode vs CLI mode + - Test all edge cases from checklist + +2. Verify against original functionality + - Use the functionality checklist + - Ensure no regressions + - Test with real task data + +3. Performance testing + - Compare execution time with old implementation + - Ensure context gathering performs well + +**Tests to Create**: +- `update-task.integration.spec.ts` - Full workflow tests +- End-to-end tests with real task files + +--- + +### Phase 10: Documentation & Migration + +**Goal**: Document the new system and deprecate old code + +**Tasks**: +1. Update documentation + - Update `apps/docs/command-reference.mdx` + - Add JSDoc comments to all public APIs + - Create migration guide for users + +2. Add deprecation warnings + - Mark old `update` and `update-subtask` commands as deprecated + - Add console warnings directing users to new command + +3. Create changeset + - Document breaking changes (if any) + - Document new features (unified command) + - Note backward compatibility + +**Files to Modify**: +1. `apps/docs/command-reference.mdx` - Update command documentation +2. Legacy files (add deprecation warnings): + - `scripts/modules/task-manager/update-tasks.js` + - `scripts/modules/task-manager/update-subtask-by-id.js` + +--- + +### Phase 11: Cleanup + +**Goal**: Remove deprecated code (future version) + +**Tasks**: +1. Remove old files: + - `scripts/modules/task-manager/update-tasks.js` + - `scripts/modules/task-manager/update-subtask-by-id.js` + - Any related old command handlers + +2. Clean up any temporary compatibility shims + +3. Update all references in codebase to use new command + +--- + +## Testing Strategy + +### Unit Tests + +- [ ] Mode detection logic +- [ ] ID parsing and validation +- [ ] Context gathering integration +- [ ] Prompt building for each mode +- [ ] Data merging logic + +### Integration Tests + +- [ ] Bulk update workflow +- [ ] Single task update workflow +- [ ] Single subtask update workflow +- [ ] MCP mode operation +- [ ] CLI mode operation + +### Edge Cases + +- [ ] Empty tasks.json +- [ ] Invalid ID formats +- [ ] Non-existent IDs +- [ ] Tasks with no subtasks +- [ ] Empty AI responses +- [ ] Context gathering failures + +--- + +## Backward Compatibility + +### Deprecation Strategy + +1. Keep old commands working initially +2. Add deprecation warnings +3. Update all documentation +4. Remove old commands in next major version + +### Alias Support (Optional) + +```bash +# Could maintain old command names as aliases +task-master update --from=3 --prompt="..." # Still works, calls update-task +task-master update-subtask --id=3.2 --prompt="..." # Still works, calls update-task +``` + +--- + +## Risk Mitigation + +### High-Risk Areas + +1. **Data integrity**: Ensure writeJSON doesn't corrupt existing data +2. **AI service compatibility**: Both generateObjectService and generateTextService must work +3. **Subtask detail format**: Maintain timestamp format consistency +4. **Context gathering**: Same behavior across all modes + +### Rollback Plan + +- Keep old files until new version is fully tested +- Version bump allows reverting if issues found +- Comprehensive test coverage before release + +--- + +## Success Criteria + +- [ ] All checklist items verified working +- [ ] Tests passing for all modes +- [ ] MCP integration functional +- [ ] CLI display matches existing behavior +- [ ] Documentation updated +- [ ] No regression in existing functionality +- [ ] Performance comparable or better than current implementation