chore: refactor tm-core to host more of our "core" commands (#1331)

This commit is contained in:
Ralph Khreish
2025-10-21 21:16:15 +02:00
committed by GitHub
parent c1e2811d2b
commit 03b7ef9a0e
146 changed files with 3137 additions and 1009 deletions

View File

@@ -6,15 +6,15 @@ on:
- main - main
- next - next
paths: paths:
- 'apps/extension/**' - "apps/extension/**"
- '.github/workflows/extension-ci.yml' - ".github/workflows/extension-ci.yml"
pull_request: pull_request:
branches: branches:
- main - main
- next - next
paths: paths:
- 'apps/extension/**' - "apps/extension/**"
- '.github/workflows/extension-ci.yml' - ".github/workflows/extension-ci.yml"
permissions: permissions:
contents: read contents: read
@@ -55,7 +55,6 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: Restore node_modules - name: Restore node_modules
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -72,7 +71,7 @@ jobs:
- name: Type Check Extension - name: Type Check Extension
working-directory: apps/extension working-directory: apps/extension
run: npm run check-types run: npm run typecheck
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
@@ -86,7 +85,6 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: Restore node_modules - name: Restore node_modules
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -137,4 +135,3 @@ jobs:
apps/extension/vsix-build/*.vsix apps/extension/vsix-build/*.vsix
apps/extension/dist/ apps/extension/dist/
retention-days: 30 retention-days: 30

View File

@@ -37,7 +37,7 @@ jobs:
- name: Type Check Extension - name: Type Check Extension
working-directory: apps/extension working-directory: apps/extension
run: npm run check-types run: npm run typecheck
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
@@ -107,4 +107,4 @@ jobs:
echo "🎉 Extension ${{ github.ref_name }} successfully published!" echo "🎉 Extension ${{ github.ref_name }} successfully published!"
echo "📦 Available on VS Code Marketplace" echo "📦 Available on VS Code Marketplace"
echo "🌍 Available on Open VSX Registry" echo "🌍 Available on Open VSX Registry"
echo "🏷️ GitHub release created: ${{ github.ref_name }}" echo "🏷️ GitHub release created: ${{ github.ref_name }}"

View File

@@ -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 Guidelines
- **Documentation location**: Write docs in `apps/docs/` (Mintlify site source), not `docs/` - **Documentation location**: Write docs in `apps/docs/` (Mintlify site source), not `docs/`

View File

@@ -12,7 +12,7 @@ import {
AuthManager, AuthManager,
AuthenticationError, AuthenticationError,
type AuthCredentials type AuthCredentials
} from '@tm/core/auth'; } from '@tm/core';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { ContextCommand } from './context.command.js'; import { ContextCommand } from './context.command.js';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';

View File

@@ -8,12 +8,7 @@ import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import ora, { type Ora } from 'ora'; import ora, { type Ora } from 'ora';
import { import { createTmCore, type TmCore, type Task, type Subtask } from '@tm/core';
createTaskMasterCore,
type TaskMasterCore,
type Task,
type Subtask
} from '@tm/core';
import * as ui from '../utils/ui.js'; 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 * This is a thin presentation layer over @tm/core's autopilot functionality
*/ */
export class AutopilotCommand extends Command { export class AutopilotCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: AutopilotCommandResult; private lastResult?: AutopilotCommandResult;
constructor(name?: string) { constructor(name?: string) {
@@ -164,11 +159,11 @@ export class AutopilotCommand extends Command {
} }
/** /**
* Initialize TaskMasterCore * Initialize TmCore
*/ */
private async initializeCore(projectRoot: string): Promise<void> { private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) { 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<Task | null> { private async loadTask(taskId: string): Promise<Task | null> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
try { try {
const { task } = await this.tmCore.getTaskWithSubtask(taskId); const { task } = await this.tmCore.tasks.get(taskId);
return task; return task;
} catch (error) { } catch (error) {
return null; return null;
@@ -236,11 +231,7 @@ export class AutopilotCommand extends Command {
} }
// Validate task structure and get execution order // Validate task structure and get execution order
const validationResult = await this.validateTaskStructure( const validationResult = await this.validateTaskStructure(taskId, task);
taskId,
task,
options
);
if (!validationResult.success) { if (!validationResult.success) {
return validationResult; return validationResult;
} }
@@ -288,19 +279,23 @@ export class AutopilotCommand extends Command {
*/ */
private async validateTaskStructure( private async validateTaskStructure(
taskId: string, taskId: string,
task: Task, task: Task
options: AutopilotCommandOptions
): Promise<AutopilotCommandResult & { orderedSubtasks?: Subtask[] }> { ): Promise<AutopilotCommandResult & { orderedSubtasks?: Subtask[] }> {
const { TaskLoaderService } = await import('@tm/core'); if (!this.tmCore) {
return {
success: false,
taskId,
task,
error: 'TmCore not initialized'
};
}
console.log(); console.log();
console.log(chalk.cyan.bold('Validating task structure...')); console.log(chalk.cyan.bold('Validating task structure...'));
const taskLoader = new TaskLoaderService(options.project || process.cwd()); const validationResult = await this.tmCore.tasks.loadAndValidate(taskId);
const validationResult = await taskLoader.loadAndValidateTask(taskId);
if (!validationResult.success) { if (!validationResult.success) {
await taskLoader.cleanup();
return { return {
success: false, success: false,
taskId, taskId,
@@ -310,12 +305,10 @@ export class AutopilotCommand extends Command {
}; };
} }
const orderedSubtasks = taskLoader.getExecutionOrder( const orderedSubtasks = this.tmCore.tasks.getExecutionOrder(
validationResult.task! validationResult.task!
); );
await taskLoader.cleanup();
return { return {
success: true, success: true,
taskId, taskId,
@@ -499,7 +492,6 @@ export class AutopilotCommand extends Command {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined; this.tmCore = undefined;
} }
} }

View File

@@ -3,7 +3,7 @@
*/ */
import { Command } from 'commander'; import { Command } from 'commander';
import { createTaskMasterCore, type WorkflowContext } from '@tm/core'; import { createTmCore, type WorkflowContext } from '@tm/core';
import { import {
AutopilotBaseOptions, AutopilotBaseOptions,
hasWorkflowState, hasWorkflowState,
@@ -67,20 +67,19 @@ export class StartCommand extends Command {
} }
// Initialize Task Master Core // Initialize Task Master Core
const tmCore = await createTaskMasterCore({ const tmCore = await createTmCore({
projectPath: mergedOptions.projectRoot! projectPath: mergedOptions.projectRoot!
}); });
// Get current tag from ConfigManager // Get current tag from ConfigManager
const currentTag = tmCore.getActiveTag(); const currentTag = tmCore.config.getActiveTag();
// Load task // Load task
formatter.info(`Loading task ${taskId}...`); formatter.info(`Loading task ${taskId}...`);
const { task } = await tmCore.getTaskWithSubtask(taskId); const { task } = await tmCore.tasks.get(taskId);
if (!task) { if (!task) {
formatter.error('Task not found', { taskId }); formatter.error('Task not found', { taskId });
await tmCore.close();
process.exit(1); process.exit(1);
} }
@@ -90,7 +89,6 @@ export class StartCommand extends Command {
taskId, taskId,
suggestion: `Run: task-master expand --id=${taskId}` suggestion: `Run: task-master expand --id=${taskId}`
}); });
await tmCore.close();
process.exit(1); process.exit(1);
} }
@@ -156,7 +154,6 @@ export class StartCommand extends Command {
}); });
// Clean up // Clean up
await tmCore.close();
} catch (error) { } catch (error) {
formatter.error((error as Error).message); formatter.error((error as Error).message);
if (mergedOptions.verbose) { if (mergedOptions.verbose) {

View File

@@ -8,7 +8,7 @@ import chalk from 'chalk';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import search from '@inquirer/search'; import search from '@inquirer/search';
import ora, { Ora } from 'ora'; 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 * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';

View File

@@ -7,8 +7,13 @@ import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import ora, { Ora } from 'ora'; import ora, { Ora } from 'ora';
import { AuthManager, type UserContext } from '@tm/core/auth'; import {
import { TaskMasterCore, type ExportResult } from '@tm/core'; AuthManager,
type UserContext,
type ExportResult,
createTmCore,
type TmCore
} from '@tm/core';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';
@@ -28,7 +33,7 @@ export interface ExportCommandResult {
*/ */
export class ExportCommand extends Command { export class ExportCommand extends Command {
private authManager: AuthManager; private authManager: AuthManager;
private taskMasterCore?: TaskMasterCore; private taskMasterCore?: TmCore;
private lastResult?: ExportCommandResult; private lastResult?: ExportCommandResult;
constructor(name?: string) { constructor(name?: string) {
@@ -61,7 +66,7 @@ export class ExportCommand extends Command {
} }
/** /**
* Initialize the TaskMasterCore * Initialize the TmCore
*/ */
private async initializeServices(): Promise<void> { private async initializeServices(): Promise<void> {
if (this.taskMasterCore) { if (this.taskMasterCore) {
@@ -69,8 +74,8 @@ export class ExportCommand extends Command {
} }
try { try {
// Initialize TaskMasterCore // Initialize TmCore
this.taskMasterCore = await TaskMasterCore.create({ this.taskMasterCore = await createTmCore({
projectPath: process.cwd() projectPath: process.cwd()
}); });
} catch (error) { } catch (error) {
@@ -152,7 +157,8 @@ export class ExportCommand extends Command {
// Perform export // Perform export
spinner = ora('Exporting tasks...').start(); spinner = ora('Exporting tasks...').start();
const exportResult = await this.taskMasterCore!.exportTasks({ // Use integration domain facade
const exportResult = await this.taskMasterCore!.integration.exportTasks({
orgId, orgId,
briefId, briefId,
tag: options?.tag, tag: options?.tag,

View File

@@ -6,16 +6,16 @@
import { Command } from 'commander'; import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import { import {
createTaskMasterCore, createTmCore,
type Task, type Task,
type TaskStatus, type TaskStatus,
type TaskMasterCore, type TmCore,
TASK_STATUSES, TASK_STATUSES,
OUTPUT_FORMATS, OUTPUT_FORMATS,
STATUS_ICONS, STATUS_ICONS,
type OutputFormat type OutputFormat
} from '@tm/core'; } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';
import { displayCommandHeader } from '../utils/display-helpers.js'; import { displayCommandHeader } from '../utils/display-helpers.js';
@@ -59,7 +59,7 @@ export interface ListTasksResult {
* This is a thin presentation layer over @tm/core * This is a thin presentation layer over @tm/core
*/ */
export class ListTasksCommand extends Command { export class ListTasksCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: ListTasksResult; private lastResult?: ListTasksResult;
constructor(name?: string) { constructor(name?: string) {
@@ -144,11 +144,11 @@ export class ListTasksCommand extends Command {
} }
/** /**
* Initialize TaskMasterCore * Initialize TmCore
*/ */
private async initializeCore(projectRoot: string): Promise<void> { private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) { 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 options: ListCommandOptions
): Promise<ListTasksResult> { ): Promise<ListTasksResult> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
// Build filter // Build filter
@@ -173,7 +173,7 @@ export class ListTasksCommand extends Command {
: undefined; : undefined;
// Call tm-core // Call tm-core
const result = await this.tmCore.getTaskList({ const result = await this.tmCore.tasks.list({
tag: options.tag, tag: options.tag,
filter, filter,
includeSubtasks: options.withSubtasks includeSubtasks: options.withSubtasks
@@ -459,7 +459,6 @@ export class ListTasksCommand extends Command {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined; this.tmCore = undefined;
} }
} }

View File

@@ -7,8 +7,8 @@ import path from 'node:path';
import { Command } from 'commander'; import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; import { createTmCore, type Task, type TmCore } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';
import { displayTaskDetails } from '../ui/components/task-detail.component.js'; import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayCommandHeader } from '../utils/display-helpers.js'; import { displayCommandHeader } from '../utils/display-helpers.js';
@@ -38,7 +38,7 @@ export interface NextTaskResult {
* This is a thin presentation layer over @tm/core * This is a thin presentation layer over @tm/core
*/ */
export class NextCommand extends Command { export class NextCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: NextTaskResult; private lastResult?: NextTaskResult;
constructor(name?: string) { constructor(name?: string) {
@@ -104,12 +104,12 @@ export class NextCommand extends Command {
} }
/** /**
* Initialize TaskMasterCore * Initialize TmCore
*/ */
private async initializeCore(projectRoot: string): Promise<void> { private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) { if (!this.tmCore) {
const resolved = path.resolve(projectRoot); 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 options: NextCommandOptions
): Promise<NextTaskResult> { ): Promise<NextTaskResult> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
// Call tm-core to get next task // 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 // Get storage type and active tag
const storageType = this.tmCore.getStorageType(); const storageType = this.tmCore.config.getStorageConfig().type;
if (storageType === 'auto') { if (storageType === 'auto') {
throw new Error('Storage type must be resolved before use'); 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 { return {
task, task,
@@ -232,7 +232,6 @@ export class NextCommand extends Command {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined; this.tmCore = undefined;
} }
} }

View File

@@ -6,12 +6,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import { import { createTmCore, type TmCore, type TaskStatus } from '@tm/core';
createTaskMasterCore, import type { StorageType } from '@tm/core';
type TaskMasterCore,
type TaskStatus
} from '@tm/core';
import type { StorageType } from '@tm/core/types';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';
/** /**
@@ -56,7 +52,7 @@ export interface SetStatusResult {
* This is a thin presentation layer over @tm/core * This is a thin presentation layer over @tm/core
*/ */
export class SetStatusCommand extends Command { export class SetStatusCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: SetStatusResult; private lastResult?: SetStatusResult;
constructor(name?: string) { constructor(name?: string) {
@@ -112,7 +108,7 @@ export class SetStatusCommand extends Command {
} }
// Initialize TaskMaster core // Initialize TaskMaster core
this.tmCore = await createTaskMasterCore({ this.tmCore = await createTmCore({
projectPath: options.project || process.cwd() projectPath: options.project || process.cwd()
}); });
@@ -128,7 +124,7 @@ export class SetStatusCommand extends Command {
for (const taskId of taskIds) { for (const taskId of taskIds) {
try { try {
const result = await this.tmCore.updateTaskStatus( const result = await this.tmCore.tasks.updateStatus(
taskId, taskId,
options.status options.status
); );
@@ -168,7 +164,7 @@ export class SetStatusCommand extends Command {
this.lastResult = { this.lastResult = {
success: true, success: true,
updatedTasks, updatedTasks,
storageType: this.tmCore.getStorageType() as Exclude< storageType: this.tmCore.config.getStorageConfig().type as Exclude<
StorageType, StorageType,
'auto' 'auto'
> >
@@ -188,7 +184,6 @@ export class SetStatusCommand extends Command {
} finally { } finally {
// Clean up resources // Clean up resources
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
} }
} }

View File

@@ -6,8 +6,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; import { createTmCore, type Task, type TmCore } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js'; import { displayError } from '../utils/error-handler.js';
import { displayTaskDetails } from '../ui/components/task-detail.component.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 * This is a thin presentation layer over @tm/core
*/ */
export class ShowCommand extends Command { export class ShowCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: ShowTaskResult | ShowMultipleTasksResult; private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
constructor(name?: string) { constructor(name?: string) {
@@ -133,11 +133,11 @@ export class ShowCommand extends Command {
} }
/** /**
* Initialize TaskMasterCore * Initialize TmCore
*/ */
private async initializeCore(projectRoot: string): Promise<void> { private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) { 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 _options: ShowCommandOptions
): Promise<ShowTaskResult> { ): Promise<ShowTaskResult> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
// Get the task // Get the task
const task = await this.tmCore.getTask(taskId); const result = await this.tmCore.tasks.get(taskId);
// Get storage type // Get storage type
const storageType = this.tmCore.getStorageType(); const storageType = this.tmCore.config.getStorageConfig().type;
return { return {
task, task: result.task,
found: task !== null, found: result.task !== null,
storageType: storageType as Exclude<StorageType, 'auto'> storageType: storageType as Exclude<StorageType, 'auto'>
}; };
} }
@@ -173,7 +173,7 @@ export class ShowCommand extends Command {
_options: ShowCommandOptions _options: ShowCommandOptions
): Promise<ShowMultipleTasksResult> { ): Promise<ShowMultipleTasksResult> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
const tasks: Task[] = []; const tasks: Task[] = [];
@@ -181,16 +181,16 @@ export class ShowCommand extends Command {
// Get each task individually // Get each task individually
for (const taskId of taskIds) { for (const taskId of taskIds) {
const task = await this.tmCore.getTask(taskId); const result = await this.tmCore.tasks.get(taskId);
if (task) { if (result.task) {
tasks.push(task); tasks.push(result.task);
} else { } else {
notFound.push(taskId); notFound.push(taskId);
} }
} }
// Get storage type // Get storage type
const storageType = this.tmCore.getStorageType(); const storageType = this.tmCore.config.getStorageConfig().type;
return { return {
tasks, tasks,
@@ -253,7 +253,7 @@ export class ShowCommand extends Command {
} }
// Display header with storage info // Display header with storage info
const activeTag = this.tmCore?.getActiveTag() || 'master'; const activeTag = this.tmCore?.config.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, { displayCommandHeader(this.tmCore, {
tag: activeTag, tag: activeTag,
storageType: result.storageType storageType: result.storageType
@@ -276,7 +276,7 @@ export class ShowCommand extends Command {
_options: ShowCommandOptions _options: ShowCommandOptions
): void { ): void {
// Display header with storage info // Display header with storage info
const activeTag = this.tmCore?.getActiveTag() || 'master'; const activeTag = this.tmCore?.config.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, { displayCommandHeader(this.tmCore, {
tag: activeTag, tag: activeTag,
storageType: result.storageType storageType: result.storageType
@@ -322,7 +322,6 @@ export class ShowCommand extends Command {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined; this.tmCore = undefined;
} }
} }

View File

@@ -10,8 +10,8 @@ import boxen from 'boxen';
import ora, { type Ora } from 'ora'; import ora, { type Ora } from 'ora';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { import {
createTaskMasterCore, createTmCore,
type TaskMasterCore, type TmCore,
type StartTaskResult as CoreStartTaskResult type StartTaskResult as CoreStartTaskResult
} from '@tm/core'; } from '@tm/core';
import { displayTaskDetails } from '../ui/components/task-detail.component.js'; 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 * This is a thin presentation layer over @tm/core's TaskExecutionService
*/ */
export class StartCommand extends Command { export class StartCommand extends Command {
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
private lastResult?: StartCommandResult; private lastResult?: StartCommandResult;
constructor(name?: string) { constructor(name?: string) {
@@ -147,7 +147,7 @@ export class StartCommand extends Command {
// Convert core result to CLI result with storage type // Convert core result to CLI result with storage type
const result: StartCommandResult = { const result: StartCommandResult = {
...coreResult, ...coreResult,
storageType: this.tmCore?.getStorageType() storageType: this.tmCore?.config.getStorageConfig().type
}; };
// Store result for programmatic access // Store result for programmatic access
@@ -180,11 +180,11 @@ export class StartCommand extends Command {
} }
/** /**
* Initialize TaskMasterCore * Initialize TmCore
*/ */
private async initializeCore(projectRoot: string): Promise<void> { private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) { 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<string | null> { private async performGetNextTask(): Promise<string | null> {
if (!this.tmCore) { 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<void> { private async showPreLaunchMessage(targetTaskId: string): Promise<void> {
if (!this.tmCore) return; if (!this.tmCore) return;
const { task, subtask, subtaskId } = const { task, isSubtask } = await this.tmCore.tasks.get(targetTaskId);
await this.tmCore.getTaskWithSubtask(targetTaskId);
if (task) { if (task) {
const workItemText = subtask const workItemText = isSubtask
? `Subtask #${task.id}.${subtaskId} - ${subtask.title}` ? `Subtask #${targetTaskId} - ${task.title}`
: `Task #${task.id} - ${task.title}`; : `Task #${task.id} - ${task.title}`;
console.log( console.log(
@@ -227,7 +226,7 @@ export class StartCommand extends Command {
options: StartCommandOptions options: StartCommandOptions
): Promise<CoreStartTaskResult> { ): Promise<CoreStartTaskResult> {
if (!this.tmCore) { if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized'); throw new Error('TmCore not initialized');
} }
// Show spinner for status update if enabled // 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) // 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, dryRun: options.dryRun,
force: options.force, force: options.force,
updateStatus: !options.noStatusUpdate updateStatus: !options.noStatusUpdate
@@ -471,7 +470,6 @@ export class StartCommand extends Command {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined; this.tmCore = undefined;
} }
} }

View File

@@ -41,5 +41,5 @@ export type {
Task, Task,
TaskStatus, TaskStatus,
TaskPriority, TaskPriority,
TaskMasterCore TmCore
} from '@tm/core'; } from '@tm/core';

View File

@@ -5,7 +5,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; 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'; import { getComplexityWithColor } from '../../utils/ui.js';
/** /**

View File

@@ -5,7 +5,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import type { Task } from '@tm/core/types'; import type { Task } from '@tm/core';
import { getComplexityWithColor, getBoxWidth } from '../../utils/ui.js'; import { getComplexityWithColor, getBoxWidth } from '../../utils/ui.js';
/** /**

View File

@@ -8,7 +8,7 @@ import boxen from 'boxen';
import Table from 'cli-table3'; import Table from 'cli-table3';
import { marked, MarkedExtension } from 'marked'; import { marked, MarkedExtension } from 'marked';
import { markedTerminal } from 'marked-terminal'; import { markedTerminal } from 'marked-terminal';
import type { Task } from '@tm/core/types'; import type { Task } from '@tm/core';
import { import {
getStatusWithColor, getStatusWithColor,
getPriorityWithColor, getPriorityWithColor,

View File

@@ -3,73 +3,41 @@
* Provides DRY utilities for displaying headers and other command output * Provides DRY utilities for displaying headers and other command output
*/ */
import type { TaskMasterCore } from '@tm/core'; import type { TmCore } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core';
import { displayHeader, type BriefInfo } from '../ui/index.js'; import { displayHeader } 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}`;
}
/** /**
* Display the command header with appropriate storage information * Display the command header with appropriate storage information
* Handles both API and file storage displays * Handles both API and file storage displays
*/ */
export function displayCommandHeader( export function displayCommandHeader(
tmCore: TaskMasterCore | undefined, tmCore: TmCore | undefined,
options: { options: {
tag?: string; tag?: string;
storageType: Exclude<StorageType, 'auto'>; storageType: Exclude<StorageType, 'auto'>;
} }
): void { ): void {
const { tag, storageType } = options; if (!tmCore) {
// Fallback display if tmCore is not available
// Get brief info if using API storage displayHeader({
let briefInfo: BriefInfo | undefined; tag: options.tag || 'master',
if (storageType === 'api' && tmCore) { storageType: options.storageType
const storageInfo = tmCore.getStorageDisplayInfo(); });
if (storageInfo) { return;
// Construct full brief info with web app URL
briefInfo = {
...storageInfo,
webAppUrl: getWebAppUrl()
};
}
} }
// Get file path for display (only for file storage) // Get the resolved storage type from tasks domain
// Note: The file structure is fixed for file storage and won't change. const resolvedStorageType = tmCore.tasks.getStorageType();
// This is a display-only relative path, not used for actual file operations.
const filePath =
storageType === 'file' && tmCore
? `.taskmaster/tasks/tasks.json`
: undefined;
// 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({ displayHeader({
tag: tag || 'master', tag: options.tag || 'master',
filePath: filePath, filePath: displayInfo.filePath,
storageType: storageType === 'api' ? 'api' : 'file', storageType: displayInfo.storageType,
briefInfo: briefInfo briefInfo: displayInfo.briefInfo
}); });
} }

View File

@@ -6,7 +6,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import Table from 'cli-table3'; 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) * Get colored status display with ASCII icons (matches scripts/modules/ui.js style)

View File

@@ -181,7 +181,7 @@ Workflows upload artifacts that you can download:
- Check extension code compiles locally: `cd apps/extension && npm run build` - Check extension code compiles locally: `cd apps/extension && npm run build`
- Verify tests pass locally: `npm run test` - Verify tests pass locally: `npm run test`
- Check for TypeScript errors: `npm run check-types` - Check for TypeScript errors: `npm run typecheck`
#### Packaging Failures #### Packaging Failures

View File

@@ -61,7 +61,7 @@ npm run build:css
npm run build npm run build
# Type checking # Type checking
npm run check-types npm run typecheck
# Linting # Linting
npm run lint npm run lint

View File

@@ -237,7 +237,7 @@
"watch": "npm run watch:js & npm run watch:css", "watch": "npm run watch:js & npm run watch:css",
"watch:js": "node ./esbuild.js --watch", "watch:js": "node ./esbuild.js --watch",
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch", "watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
"check-types": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -4,7 +4,7 @@
*/ */
import * as vscode from 'vscode'; 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'; import type { ExtensionLogger } from '../utils/logger';
export interface TerminalExecutionOptions { export interface TerminalExecutionOptions {
@@ -21,7 +21,7 @@ export interface TerminalExecutionResult {
export class TerminalManager { export class TerminalManager {
private terminals = new Map<string, vscode.Terminal>(); private terminals = new Map<string, vscode.Terminal>();
private tmCore?: TaskMasterCore; private tmCore?: TmCore;
constructor( constructor(
private context: vscode.ExtensionContext, private context: vscode.ExtensionContext,
@@ -49,7 +49,7 @@ export class TerminalManager {
await this.initializeCore(); await this.initializeCore();
// Use tm-core to start the task (same as CLI) // 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, dryRun: false,
force: false, force: false,
updateStatus: true updateStatus: true
@@ -110,7 +110,7 @@ export class TerminalManager {
if (!workspaceRoot) { if (!workspaceRoot) {
throw new Error('No workspace folder found'); 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(); this.terminals.clear();
// Clear tm-core reference (no explicit cleanup needed)
if (this.tmCore) { if (this.tmCore) {
try { this.tmCore = undefined;
await this.tmCore.close();
this.tmCore = undefined;
} catch (error) {
this.logger.error('Failed to close tm-core:', error);
}
} }
} }
} }

View File

@@ -11,7 +11,7 @@ import {
withNormalizedProjectRoot withNormalizedProjectRoot
} from '../../shared/utils.js'; } from '../../shared/utils.js';
import type { MCPContext } from '../../shared/types.js'; import type { MCPContext } from '../../shared/types.js';
import { createTaskMasterCore } from '@tm/core'; import { createTmCore } from '@tm/core';
import { WorkflowService } from '@tm/core'; import { WorkflowService } from '@tm/core';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
@@ -83,17 +83,16 @@ export function registerAutopilotStartTool(server: FastMCP) {
} }
// Load task data and get current tag // Load task data and get current tag
const core = await createTaskMasterCore({ const core = await createTmCore({
projectPath: projectRoot projectPath: projectRoot
}); });
// Get current tag from ConfigManager // 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) { if (!taskResult || !taskResult.task) {
await core.close();
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,
@@ -108,7 +107,6 @@ export function registerAutopilotStartTool(server: FastMCP) {
// Validate task has subtasks // Validate task has subtasks
if (!task.subtasks || task.subtasks.length === 0) { if (!task.subtasks || task.subtasks.length === 0) {
await core.close();
return handleApiResult({ return handleApiResult({
result: { result: {
success: false, success: false,

1
package-lock.json generated
View File

@@ -79,6 +79,7 @@
"@manypkg/cli": "^0.25.1", "@manypkg/cli": "^0.25.1",
"@tm/ai-sdk-provider-grok-cli": "*", "@tm/ai-sdk-provider-grok-cli": "*",
"@tm/cli": "*", "@tm/cli": "*",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1", "@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View File

@@ -139,6 +139,7 @@
"@manypkg/cli": "^0.25.1", "@manypkg/cli": "^0.25.1",
"@tm/ai-sdk-provider-grok-cli": "*", "@tm/ai-sdk-provider-grok-cli": "*",
"@tm/cli": "*", "@tm/cli": "*",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1", "@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

View File

@@ -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';

View File

@@ -8,8 +8,8 @@ export type * from './storage.interface.js';
export * from './storage.interface.js'; export * from './storage.interface.js';
// AI Provider interfaces // AI Provider interfaces
export type * from './ai-provider.interface.js'; export type * from '../../modules/ai/interfaces/ai-provider.interface.js';
export * from './ai-provider.interface.js'; export * from '../../modules/ai/interfaces/ai-provider.interface.js';
// Configuration interfaces // Configuration interfaces
export type * from './configuration.interface.js'; export type * from './configuration.interface.js';

View File

@@ -164,6 +164,12 @@ export interface IStorage {
* @returns Promise that resolves to storage statistics * @returns Promise that resolves to storage statistics
*/ */
getStats(): Promise<StorageStats>; getStats(): Promise<StorageStats>;
/**
* 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<void>; abstract initialize(): Promise<void>;
abstract close(): Promise<void>; abstract close(): Promise<void>;
abstract getStats(): Promise<StorageStats>; abstract getStats(): Promise<StorageStats>;
abstract getStorageType(): 'file' | 'api';
/** /**
* Utility method to generate backup filename * Utility method to generate backup filename

View File

@@ -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';

View File

@@ -1,117 +1,127 @@
/** /**
* @fileoverview Main entry point for the tm-core package * @fileoverview Main entry point for @tm/core
* This file exports all public APIs from the core Task Master library * Provides unified access to all Task Master functionality through TmCore
*/ */
// Export main facade import type { TasksDomain } from './modules/tasks/tasks-domain.js';
export {
TaskMasterCore,
createTaskMasterCore,
type TaskMasterCoreOptions,
type ListTasksResult,
type StartTaskOptions,
type StartTaskResult,
type ConflictCheckResult,
type ExportTasksOptions,
type ExportResult
} from './task-master-core.js';
// Re-export types // ========== Primary API ==========
export type * from './types/index.js';
// 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 // ========== Type Exports ==========
export * from './constants/index.js';
// Re-export providers // Common types that consumers need
export * from './providers/index.js'; export type * from './common/types/index.js';
// Re-export storage (selectively to avoid conflicts) // Common interfaces
export { export type * from './common/interfaces/index.js';
FileStorage,
ApiStorage,
StorageFactory,
type ApiStorageConfig
} from './storage/index.js';
export { PlaceholderStorage, type StorageAdapter } from './storage/index.js';
// Re-export parser // Constants
export * from './parser/index.js'; export * from './common/constants/index.js';
// Re-export utilities // Errors
export * from './utils/index.js'; export * from './common/errors/index.js';
// Re-export errors // ========== Domain-Specific Type Exports ==========
export * from './errors/index.js';
// Re-export entities // Task types
export { TaskEntity } from './entities/task.entity.js'; export type {
TaskListResult,
GetTaskListOptions
} from './modules/tasks/services/task-service.js';
// Re-export authentication export type {
export { StartTaskOptions,
AuthManager, StartTaskResult,
AuthenticationError, ConflictCheckResult
type AuthCredentials, } from './modules/tasks/services/task-execution-service.js';
type OAuthFlowOptions,
type AuthConfig
} from './auth/index.js';
// Re-export logger export type {
export { getLogger, createLogger, setGlobalLogger } from './logger/index.js'; PreflightResult,
CheckResult
} from './modules/tasks/services/preflight-checker.service.js';
// Re-export executors // Task domain result types
export * from './executors/index.js'; export type TaskWithSubtaskResult = Awaited<ReturnType<TasksDomain['get']>>;
// Re-export reports // Auth types
export { export type {
ComplexityReportManager, AuthCredentials,
type ComplexityReport, OAuthFlowOptions,
type ComplexityReportMetadata, UserContext
type ComplexityAnalysis, } from './modules/auth/types.js';
type TaskComplexityData export { AuthenticationError } from './modules/auth/types.js';
} from './reports/index.js';
// Re-export services // Workflow types
export { export type {
PreflightChecker, StartWorkflowOptions,
TaskLoaderService, WorkflowStatus,
type CheckResult, NextAction
type PreflightResult, } from './modules/workflow/services/workflow.service.js';
type TaskValidationResult,
type ValidationErrorType,
type DependencyIssue
} from './services/index.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 { export type {
WorkflowPhase, WorkflowPhase,
TDDPhase, TDDPhase,
WorkflowContext, WorkflowContext,
WorkflowState, WorkflowState,
WorkflowEvent, TestResult
WorkflowEventData, } from './modules/workflow/types.js';
WorkflowEventListener,
SubtaskInfo,
TestResult,
WorkflowError
} from './workflow/types.js';
// Re-export workflow service // Git types
export { WorkflowService } from './services/workflow.service.js'; export type { CommitMessageOptions } from './modules/git/services/commit-message-generator.js';
// Integration types
export type { export type {
StartWorkflowOptions, ExportTasksOptions,
WorkflowStatus, ExportResult
NextAction } from './modules/integration/services/export.service.js';
} from './services/workflow.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';

View File

@@ -3,7 +3,7 @@
*/ */
// Export all from AI module // Export all from AI module
export * from './ai/index.js'; export * from './providers/index.js';
// Storage providers will be exported here when implemented // Storage providers will be exported here when implemented
// export * from './storage/index.js'; // export * from './storage/index.js';

View File

@@ -6,12 +6,15 @@
import { import {
ERROR_CODES, ERROR_CODES,
TaskMasterError TaskMasterError
} from '../../errors/task-master-error.js'; } from '../../../common/errors/task-master-error.js';
import type { import type {
AIOptions, AIOptions,
AIResponse, AIResponse,
IAIProvider IAIProvider,
} from '../../interfaces/ai-provider.interface.js'; ProviderUsageStats,
ProviderInfo,
AIModel
} from '../interfaces/ai-provider.interface.js';
// Constants for retry logic // Constants for retry logic
const DEFAULT_MAX_RETRIES = 3; const DEFAULT_MAX_RETRIES = 3;
@@ -428,17 +431,10 @@ export abstract class BaseProvider implements IAIProvider {
options?: AIOptions options?: AIOptions
): AsyncIterator<Partial<AIResponse>>; ): AsyncIterator<Partial<AIResponse>>;
abstract isAvailable(): Promise<boolean>; abstract isAvailable(): Promise<boolean>;
abstract getProviderInfo(): import( abstract getProviderInfo(): ProviderInfo;
'../../interfaces/ai-provider.interface.js' abstract getAvailableModels(): AIModel[];
).ProviderInfo;
abstract getAvailableModels(): import(
'../../interfaces/ai-provider.interface.js'
).AIModel[];
abstract validateCredentials(): Promise<boolean>; abstract validateCredentials(): Promise<boolean>;
abstract getUsageStats(): Promise< abstract getUsageStats(): Promise<ProviderUsageStats | null>;
| import('../../interfaces/ai-provider.interface.js').ProviderUsageStats
| null
>;
abstract initialize(): Promise<void>; abstract initialize(): Promise<void>;
abstract close(): Promise<void>; abstract close(): Promise<void>;
} }

View File

@@ -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<StorageType, 'auto'>;
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<AuthCredentials> {
return this.authManager.authenticateWithOAuth(options);
}
/**
* Get OAuth authorization URL
*/
getAuthorizationUrl(): string | null {
return this.authManager.getAuthorizationUrl();
}
/**
* Refresh authentication token
*/
async refreshToken(): Promise<AuthCredentials> {
return this.authManager.refreshToken();
}
/**
* Logout current user
*/
async logout(): Promise<void> {
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<UserContext>): 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<Organization[]> {
return this.authManager.getOrganizations();
}
/**
* Get a specific organization by ID
*/
async getOrganization(orgId: string): Promise<Organization | null> {
return this.authManager.getOrganization(orgId);
}
/**
* Get all briefs for a specific organization
*/
async getBriefs(orgId: string): Promise<Brief[]> {
return this.authManager.getBriefs(orgId);
}
/**
* Get a specific brief by ID
*/
async getBrief(briefId: string): Promise<Brief | null> {
return this.authManager.getBrief(briefId);
}
/**
* Get all tasks for a specific brief
*/
async getTasks(briefId: string): Promise<RemoteTask[]> {
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}`;
}
}

View File

@@ -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';

View File

@@ -8,17 +8,17 @@ import {
AuthenticationError, AuthenticationError,
AuthConfig, AuthConfig,
UserContext UserContext
} from './types.js'; } from '../types.js';
import { CredentialStore } from './credential-store.js'; import { CredentialStore } from '../services/credential-store.js';
import { OAuthService } from './oauth-service.js'; import { OAuthService } from '../services/oauth-service.js';
import { SupabaseAuthClient } from '../clients/supabase-client.js'; import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
import { import {
OrganizationService, OrganizationService,
type Organization, type Organization,
type Brief, type Brief,
type RemoteTask type RemoteTask
} from '../services/organization.service.js'; } from '../services/organization.service.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
/** /**
* Authentication manager class * Authentication manager class

View File

@@ -3,9 +3,9 @@
*/ */
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { CredentialStore } from './credential-store.js'; import { CredentialStore } from '../services/credential-store.js';
import { AuthenticationError } from './types.js'; import { AuthenticationError } from '../types.js';
import type { AuthCredentials } from './types.js'; import type { AuthCredentials } from '../types.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';

View File

@@ -4,9 +4,9 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { AuthCredentials, AuthenticationError, AuthConfig } from './types.js'; import { AuthCredentials, AuthenticationError, AuthConfig } from '../types.js';
import { getAuthConfig } from './config.js'; import { getAuthConfig } from '../config.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
/** /**
* CredentialStore manages the persistence and retrieval of authentication credentials. * CredentialStore manages the persistence and retrieval of authentication credentials.

View File

@@ -12,12 +12,12 @@ import {
OAuthFlowOptions, OAuthFlowOptions,
AuthConfig, AuthConfig,
CliData CliData
} from './types.js'; } from '../types.js';
import { CredentialStore } from './credential-store.js'; import { CredentialStore } from '../services/credential-store.js';
import { SupabaseAuthClient } from '../clients/supabase-client.js'; import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
import { getAuthConfig } from './config.js'; import { getAuthConfig } from '../config.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
import packageJson from '../../../../package.json' with { type: 'json' }; import packageJson from '../../../../../../package.json' with { type: 'json' };
export class OAuthService { export class OAuthService {
private logger = getLogger('OAuthService'); private logger = getLogger('OAuthService');

View File

@@ -4,9 +4,12 @@
*/ */
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '../types/database.types.js'; import { Database } from '../../../common/types/database.types.js';
import { TaskMasterError, ERROR_CODES } from '../errors/task-master-error.js'; import {
import { getLogger } from '../logger/index.js'; TaskMasterError,
ERROR_CODES
} from '../../../common/errors/task-master-error.js';
import { getLogger } from '../../../common/logger/index.js';
/** /**
* Organization data structure * Organization data structure

View File

@@ -6,10 +6,10 @@
* auth.json credential storage, maintaining backward compatibility * 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 { CredentialStore } from './credential-store.js';
import { AuthCredentials } from './types.js'; import type { AuthCredentials } from '../types.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
const STORAGE_KEY = 'sb-taskmaster-auth-token'; const STORAGE_KEY = 'sb-taskmaster-auth-token';
@@ -29,20 +29,14 @@ export class SupabaseSessionStorage implements SupportedStorage {
const session = { const session = {
access_token: credentials.token, access_token: credentials.token,
refresh_token: credentials.refreshToken || '', refresh_token: credentials.refreshToken || '',
expires_at: credentials.expiresAt // Don't default to arbitrary values - let Supabase handle refresh
? Math.floor(new Date(credentials.expiresAt).getTime() / 1000) ...(credentials.expiresAt && {
: Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour expires_at: Math.floor(new Date(credentials.expiresAt).getTime() / 1000)
}),
token_type: 'bearer', token_type: 'bearer',
user: { user: {
id: credentials.userId, id: credentials.userId,
email: credentials.email || '', 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()
} }
}; };
return session; return session;
@@ -55,11 +49,14 @@ export class SupabaseSessionStorage implements SupportedStorage {
sessionData: any sessionData: any
): Partial<AuthCredentials> { ): Partial<AuthCredentials> {
try { 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 { return {
token: session.access_token, token: session.access_token,
refreshToken: session.refresh_token, refreshToken: session.refresh_token,
userId: session.user?.id || 'unknown', userId: session.user?.id,
email: session.user?.email, email: session.user?.email,
expiresAt: session.expires_at expiresAt: session.expires_at
? new Date(session.expires_at * 1000).toISOString() ? new Date(session.expires_at * 1000).toISOString()
@@ -78,21 +75,29 @@ export class SupabaseSessionStorage implements SupportedStorage {
// Supabase uses a specific key pattern for sessions // Supabase uses a specific key pattern for sessions
if (key === STORAGE_KEY || key.includes('auth-token')) { if (key === STORAGE_KEY || key.includes('auth-token')) {
try { try {
const credentials = this.store.getCredentials({ allowExpired: true }); // Get credentials and let Supabase handle expiry/refresh internally
if (credentials && credentials.token) { const credentials = this.store.getCredentials();
// Build and return a session object from our stored credentials
const session = this.buildSessionFromCredentials(credentials); // Only return a session if we have BOTH access token AND refresh token
return JSON.stringify(session); // 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) { } catch (error) {
this.logger.error('Error getting session:', error); this.logger.error('Error getting session:', error);
} }
} }
// Return null if no valid session exists - Supabase expects this
return null; return null;
} }
/** /**
* Set item in storage - Supabase will store the session with a specific key * 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 { setItem(key: string, value: string): void {
// Only handle Supabase session keys // Only handle Supabase session keys
@@ -102,21 +107,64 @@ export class SupabaseSessionStorage implements SupportedStorage {
// Parse the session and update our credentials // Parse the session and update our credentials
const sessionUpdates = this.parseSessionToCredentials(value); const sessionUpdates = this.parseSessionToCredentials(value);
const existingCredentials = this.store.getCredentials(); const existingCredentials = this.store.getCredentials({
allowExpired: true
});
if (sessionUpdates.token) { // CRITICAL: Only save if we have both tokens - prevents partial session states
const updatedCredentials: AuthCredentials = { // Refresh token rotation means we MUST persist the new refresh token immediately
...existingCredentials, if (!sessionUpdates.token || !sessionUpdates.refreshToken) {
...sessionUpdates, this.logger.warn(
savedAt: new Date().toISOString(), 'Received incomplete session update - skipping save to prevent token rotation issues',
selectedContext: existingCredentials?.selectedContext {
} as AuthCredentials; hasToken: !!sessionUpdates.token,
hasRefreshToken: !!sessionUpdates.refreshToken
}
);
return;
}
this.store.saveCredentials(updatedCredentials); // Log the refresh token rotation for debugging
this.logger.info( const isRotation =
'Successfully saved refreshed credentials from Supabase' 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) { } catch (error) {
this.logger.error('Error setting session:', error); this.logger.error('Error setting session:', error);
} }

View File

@@ -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';

View File

@@ -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<void> {
return this.configManager.setActiveTag(tag);
}
// ========== Configuration Updates ==========
/**
* Update configuration
*/
async updateConfig(updates: PartialConfiguration): Promise<void> {
return this.configManager.updateConfig(updates);
}
/**
* Set response language
*/
async setResponseLanguage(language: string): Promise<void> {
return this.configManager.setResponseLanguage(language);
}
/**
* Save current configuration
*/
async saveConfig(): Promise<void> {
return this.configManager.saveConfig();
}
/**
* Reset configuration to defaults
*/
async reset(): Promise<void> {
return this.configManager.reset();
}
// ========== Utilities ==========
/**
* Get configuration sources for debugging
*/
getConfigSources() {
return this.configManager.getConfigSources();
}
}

View File

@@ -4,7 +4,7 @@
*/ */
// Export the main ConfigManager // 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 all configuration services for advanced usage
export { export {
@@ -38,7 +38,7 @@ export type {
ConfigProperty, ConfigProperty,
IConfigurationFactory, IConfigurationFactory,
IConfigurationManager IConfigurationManager
} from '../interfaces/configuration.interface.js'; } from '../../common/interfaces/configuration.interface.js';
// Re-export default values // Re-export default values
export { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js'; export { DEFAULT_CONFIG_VALUES } from '../../common/interfaces/configuration.interface.js';

View File

@@ -5,19 +5,19 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { ConfigManager } from './config-manager.js'; import { ConfigManager } from './config-manager.js';
import { DEFAULT_CONFIG_VALUES } from '../interfaces/configuration.interface.js'; import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
import { ConfigLoader } from './services/config-loader.service.js'; import { ConfigLoader } from '../services/config-loader.service.js';
import { ConfigMerger } from './services/config-merger.service.js'; import { ConfigMerger } from '../services/config-merger.service.js';
import { RuntimeStateManager } from './services/runtime-state-manager.service.js'; import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
import { ConfigPersistence } from './services/config-persistence.service.js'; import { ConfigPersistence } from '../services/config-persistence.service.js';
import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js'; import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
// Mock all services // Mock all services
vi.mock('./services/config-loader.service.js'); vi.mock('../services/config-loader.service.js');
vi.mock('./services/config-merger.service.js'); vi.mock('../services/config-merger.service.js');
vi.mock('./services/runtime-state-manager.service.js'); vi.mock('../services/runtime-state-manager.service.js');
vi.mock('./services/config-persistence.service.js'); vi.mock('../services/config-persistence.service.js');
vi.mock('./services/environment-config-provider.service.js'); vi.mock('../services/environment-config-provider.service.js');
describe('ConfigManager', () => { describe('ConfigManager', () => {
let manager: ConfigManager; let manager: ConfigManager;
@@ -361,23 +361,6 @@ describe('ConfigManager', () => {
expect(sources).toEqual(mockSources); 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', () => { describe('error handling', () => {

View File

@@ -9,16 +9,16 @@
import type { import type {
PartialConfiguration, PartialConfiguration,
RuntimeStorageConfig RuntimeStorageConfig
} from '../interfaces/configuration.interface.js'; } from '../../../common/interfaces/configuration.interface.js';
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../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 { ConfigLoader } from '../services/config-loader.service.js';
import { import {
ConfigMerger, ConfigMerger,
CONFIG_PRECEDENCE CONFIG_PRECEDENCE
} from './services/config-merger.service.js'; } from '../services/config-merger.service.js';
import { RuntimeStateManager } from './services/runtime-state-manager.service.js'; import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
import { ConfigPersistence } from './services/config-persistence.service.js'; import { ConfigPersistence } from '../services/config-persistence.service.js';
import { EnvironmentConfigProvider } from './services/environment-config-provider.service.js'; import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
/** /**
* ConfigManager orchestrates all configuration services * ConfigManager orchestrates all configuration services

View File

@@ -5,7 +5,7 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import { ConfigLoader } from './config-loader.service.js'; 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', () => ({ vi.mock('node:fs', () => ({
promises: { promises: {

View File

@@ -5,12 +5,12 @@
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js';
import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
import { import {
ERROR_CODES, ERROR_CODES,
TaskMasterError TaskMasterError
} from '../../errors/task-master-error.js'; } from '../../../common/errors/task-master-error.js';
/** /**
* ConfigLoader handles loading configuration from files * ConfigLoader handles loading configuration from files

View File

@@ -3,7 +3,7 @@
* Responsible for merging configurations from multiple sources with precedence * 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 * Configuration source with precedence

View File

@@ -5,12 +5,12 @@
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js';
import { import {
ERROR_CODES, ERROR_CODES,
TaskMasterError TaskMasterError
} from '../../errors/task-master-error.js'; } from '../../../common/errors/task-master-error.js';
import { getLogger } from '../../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
/** /**
* Persistence options * Persistence options

View File

@@ -3,8 +3,8 @@
* Extracts configuration from environment variables * Extracts configuration from environment variables
*/ */
import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js';
import { getLogger } from '../../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
/** /**
* Environment variable mapping definition * Environment variable mapping definition

View File

@@ -5,7 +5,7 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import { RuntimeStateManager } from './runtime-state-manager.service.js'; 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', () => ({ vi.mock('node:fs', () => ({
promises: { promises: {

View File

@@ -8,9 +8,9 @@ import path from 'node:path';
import { import {
ERROR_CODES, ERROR_CODES,
TaskMasterError TaskMasterError
} from '../../errors/task-master-error.js'; } from '../../../common/errors/task-master-error.js';
import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
import { getLogger } from '../../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
/** /**
* Runtime state data structure * Runtime state data structure

View File

@@ -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';

View File

@@ -2,9 +2,9 @@
* Base executor class providing common functionality for all executors * Base executor class providing common functionality for all executors
*/ */
import type { Task } from '../types/index.js'; import type { Task } from '../../../common/types/index.js';
import type { ITaskExecutor, ExecutorType, ExecutionResult } from './types.js'; import type { ITaskExecutor, ExecutorType, ExecutionResult } from '../types.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
export abstract class BaseExecutor implements ITaskExecutor { export abstract class BaseExecutor implements ITaskExecutor {
protected readonly logger = getLogger('BaseExecutor'); protected readonly logger = getLogger('BaseExecutor');

View File

@@ -3,13 +3,13 @@
*/ */
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import type { Task } from '../types/index.js'; import type { Task } from '../../../common/types/index.js';
import type { import type {
ExecutorType, ExecutorType,
ExecutionResult, ExecutionResult,
ClaudeExecutorConfig ClaudeExecutorConfig
} from './types.js'; } from '../types.js';
import { BaseExecutor } from './base-executor.js'; import { BaseExecutor } from '../executors/base-executor.js';
export class ClaudeExecutor extends BaseExecutor { export class ClaudeExecutor extends BaseExecutor {
private claudeConfig: ClaudeExecutorConfig; private claudeConfig: ClaudeExecutorConfig;

View File

@@ -2,9 +2,9 @@
* Factory for creating task executors * Factory for creating task executors
*/ */
import type { ITaskExecutor, ExecutorOptions, ExecutorType } from './types.js'; import type { ITaskExecutor, ExecutorOptions, ExecutorType } from '../types.js';
import { ClaudeExecutor } from './claude-executor.js'; import { ClaudeExecutor } from '../executors/claude-executor.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
export class ExecutorFactory { export class ExecutorFactory {
private static logger = getLogger('ExecutorFactory'); private static logger = getLogger('ExecutorFactory');

View File

@@ -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';

View File

@@ -2,15 +2,15 @@
* Service for managing task execution * Service for managing task execution
*/ */
import type { Task } from '../types/index.js'; import type { Task } from '../../../common/types/index.js';
import type { import type {
ITaskExecutor, ITaskExecutor,
ExecutorOptions, ExecutorOptions,
ExecutionResult, ExecutionResult,
ExecutorType ExecutorType
} from './types.js'; } from '../types.js';
import { ExecutorFactory } from './executor-factory.js'; import { ExecutorFactory } from '../executors/executor-factory.js';
import { getLogger } from '../logger/index.js'; import { getLogger } from '../../../common/logger/index.js';
export interface ExecutorServiceOptions { export interface ExecutorServiceOptions {
projectRoot: string; projectRoot: string;

View File

@@ -2,7 +2,7 @@
* Executor types and interfaces for Task Master * 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 * Supported executor types

View File

@@ -5,7 +5,7 @@
* @module git-adapter * @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 fs from 'fs-extra';
import path from 'path'; import path from 'path';
@@ -216,14 +216,14 @@ export class GitAdapter {
* Gets the detailed status of the working tree. * Gets the detailed status of the working tree.
* Returns raw status from simple-git with all file changes. * Returns raw status from simple-git with all file changes.
* *
* @returns {Promise<import('simple-git').StatusResult>} Detailed status object * @returns {Promise<StatusResult>} Detailed status object
* *
* @example * @example
* const status = await git.getStatus(); * const status = await git.getStatus();
* console.log('Modified files:', status.modified); * console.log('Modified files:', status.modified);
* console.log('Staged files:', status.staged); * console.log('Staged files:', status.staged);
*/ */
async getStatus(): Promise<import('simple-git').StatusResult> { async getStatus(): Promise<StatusResult> {
return await this.git.status(); return await this.git.status();
} }

View File

@@ -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<boolean> {
return this.gitAdapter.isGitRepository();
}
/**
* Ensure we're in a valid git repository
*/
async ensureGitRepository(): Promise<void> {
return this.gitAdapter.ensureGitRepository();
}
/**
* Get repository root path
*/
async getRepositoryRoot(): Promise<string> {
return this.gitAdapter.getRepositoryRoot();
}
// ========== Working Tree Status ==========
/**
* Check if working tree is clean
*/
async isWorkingTreeClean(): Promise<boolean> {
return this.gitAdapter.isWorkingTreeClean();
}
/**
* Get git status
*/
async getStatus(): Promise<StatusResult> {
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<boolean> {
return this.gitAdapter.hasUncommittedChanges();
}
/**
* Check if there are staged changes
*/
async hasStagedChanges(): Promise<boolean> {
return this.gitAdapter.hasStagedChanges();
}
// ========== Branch Operations ==========
/**
* Get current branch name
*/
async getCurrentBranch(): Promise<string> {
return this.gitAdapter.getCurrentBranch();
}
/**
* List all local branches
*/
async listBranches(): Promise<string[]> {
return this.gitAdapter.listBranches();
}
/**
* Check if a branch exists
*/
async branchExists(branchName: string): Promise<boolean> {
return this.gitAdapter.branchExists(branchName);
}
/**
* Create a new branch
*/
async createBranch(
branchName: string,
options?: { checkout?: boolean }
): Promise<void> {
return this.gitAdapter.createBranch(branchName, options);
}
/**
* Checkout an existing branch
*/
async checkoutBranch(
branchName: string,
options?: { force?: boolean }
): Promise<void> {
return this.gitAdapter.checkoutBranch(branchName, options);
}
/**
* Create and checkout a new branch
*/
async createAndCheckoutBranch(branchName: string): Promise<void> {
return this.gitAdapter.createAndCheckoutBranch(branchName);
}
/**
* Delete a branch
*/
async deleteBranch(
branchName: string,
options?: { force?: boolean }
): Promise<void> {
return this.gitAdapter.deleteBranch(branchName, options);
}
/**
* Get default branch name
*/
async getDefaultBranch(): Promise<string> {
return this.gitAdapter.getDefaultBranch();
}
/**
* Check if on default branch
*/
async isOnDefaultBranch(): Promise<boolean> {
return this.gitAdapter.isOnDefaultBranch();
}
// ========== Commit Operations ==========
/**
* Stage files for commit
*/
async stageFiles(files: string[]): Promise<void> {
return this.gitAdapter.stageFiles(files);
}
/**
* Unstage files
*/
async unstageFiles(files: string[]): Promise<void> {
return this.gitAdapter.unstageFiles(files);
}
/**
* Create a commit
*/
async createCommit(
message: string,
options?: {
metadata?: Record<string, string>;
allowEmpty?: boolean;
enforceNonDefaultBranch?: boolean;
force?: boolean;
}
): Promise<void> {
return this.gitAdapter.createCommit(message, options);
}
/**
* Get commit log
*/
async getCommitLog(options?: { maxCount?: number }): Promise<any[]> {
return this.gitAdapter.getCommitLog(options);
}
/**
* Get last commit
*/
async getLastCommit(): Promise<any> {
return this.gitAdapter.getLastCommit();
}
// ========== Remote Operations ==========
/**
* Check if repository has remotes
*/
async hasRemote(): Promise<boolean> {
return this.gitAdapter.hasRemote();
}
/**
* Get all configured remotes
*/
async getRemotes(): Promise<any[]> {
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);
}
}

View File

@@ -4,10 +4,10 @@
*/ */
// Export GitAdapter // Export GitAdapter
export { GitAdapter } from './git-adapter.js'; export { GitAdapter } from './adapters/git-adapter.js';
// Export branch name utilities // Export branch name utilities
export { export {
generateBranchName, generateBranchName,
sanitizeBranchName sanitizeBranchName
} from './branch-name-generator.js'; } from './services/branch-name-generator.js';

Some files were not shown because too many files have changed in this diff Show More