feat: implement tm list with new refactored structure
This commit is contained in:
36
.vscode/settings.json
vendored
36
.vscode/settings.json
vendored
@@ -1,15 +1,27 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["src/prompts/*.json"],
|
||||
"url": "./src/prompts/schemas/prompt-template.schema.json"
|
||||
}
|
||||
],
|
||||
"files.associations": {
|
||||
"src/prompts/*.json": "json"
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["src/prompts/*.json"],
|
||||
"url": "./src/prompts/schemas/prompt-template.schema.json"
|
||||
}
|
||||
],
|
||||
"files.associations": {
|
||||
"src/prompts/*.json": "json"
|
||||
},
|
||||
|
||||
"json.format.enable": true,
|
||||
"json.validate.enable": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"json.format.enable": true,
|
||||
"json.validate.enable": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface ListCommandOptions {
|
||||
status?: string;
|
||||
tag?: string;
|
||||
withSubtasks?: boolean;
|
||||
format?: string;
|
||||
format?: OutputFormat;
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export class ListTasksCommand extends Command {
|
||||
* Execute the list command
|
||||
*/
|
||||
private async executeCommand(options: ListCommandOptions): Promise<void> {
|
||||
console.log('executeCommand', options);
|
||||
try {
|
||||
// Validate options
|
||||
if (!this.validateOptions(options)) {
|
||||
@@ -138,8 +139,7 @@ export class ListTasksCommand extends Command {
|
||||
*/
|
||||
private async initializeCore(projectRoot: string): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
this.tmCore = createTaskMasterCore(projectRoot);
|
||||
await this.tmCore.initialize();
|
||||
this.tmCore = await createTaskMasterCore({ projectPath: projectRoot });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ export function createTaskTable(
|
||||
if (showSubtasks && task.subtasks && task.subtasks.length > 0) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
const subRow: string[] = [
|
||||
chalk.gray(` └─ ${task.id}.${subtask.id}`),
|
||||
chalk.gray(` └─ ${subtask.id}`),
|
||||
chalk.gray(truncate(subtask.title, 36)),
|
||||
getStatusWithColor(subtask.status),
|
||||
chalk.gray(subtask.priority || 'medium')
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
"workspaces": ["apps/*", "packages/*", "."],
|
||||
"scripts": {
|
||||
"build": "npm run build:packages && tsup",
|
||||
"dev": "tsup --watch --onSuccess 'echo Build complete'",
|
||||
"dev": "npm run build:packages && (npm run dev:packages & tsup --watch --onSuccess 'echo Build complete')",
|
||||
"dev:packages": "(cd packages/tm-core && npm run dev) & (cd apps/cli && npm run dev) & wait",
|
||||
"dev:core": "cd packages/tm-core && npm run dev",
|
||||
"dev:cli": "cd apps/cli && npm run dev",
|
||||
"build:packages": "npm run build:core && npm run build:cli",
|
||||
"build:core": "cd packages/tm-core && npm run build",
|
||||
"build:cli": "cd apps/cli && npm run build",
|
||||
|
||||
@@ -8,39 +8,10 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/types/index.js",
|
||||
"require": "./dist/types/index.cjs"
|
||||
},
|
||||
"./providers": {
|
||||
"types": "./dist/providers/index.d.ts",
|
||||
"import": "./dist/providers/index.js",
|
||||
"require": "./dist/providers/index.cjs"
|
||||
},
|
||||
"./storage": {
|
||||
"types": "./dist/storage/index.d.ts",
|
||||
"import": "./dist/storage/index.js",
|
||||
"require": "./dist/storage/index.cjs"
|
||||
},
|
||||
"./parser": {
|
||||
"types": "./dist/parser/index.d.ts",
|
||||
"import": "./dist/parser/index.js",
|
||||
"require": "./dist/parser/index.cjs"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./dist/utils/index.d.ts",
|
||||
"import": "./dist/utils/index.js",
|
||||
"require": "./dist/utils/index.cjs"
|
||||
},
|
||||
"./errors": {
|
||||
"types": "./dist/errors/index.d.ts",
|
||||
"import": "./dist/errors/index.js",
|
||||
"require": "./dist/errors/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -25,65 +25,80 @@ describe('ConfigManager', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
|
||||
// Clear environment variables
|
||||
Object.keys(process.env).forEach(key => {
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
if (key.startsWith('TASKMASTER_')) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Setup default mock behaviors
|
||||
vi.mocked(ConfigLoader).mockImplementation(() => ({
|
||||
getDefaultConfig: vi.fn().mockReturnValue({
|
||||
models: { main: 'default-model', fallback: 'fallback-model' },
|
||||
storage: { type: 'file' },
|
||||
version: '1.0.0'
|
||||
}),
|
||||
loadLocalConfig: vi.fn().mockResolvedValue(null),
|
||||
loadGlobalConfig: vi.fn().mockResolvedValue(null),
|
||||
hasLocalConfig: vi.fn().mockResolvedValue(false),
|
||||
hasGlobalConfig: vi.fn().mockResolvedValue(false)
|
||||
} as any));
|
||||
vi.mocked(ConfigLoader).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getDefaultConfig: vi.fn().mockReturnValue({
|
||||
models: { main: 'default-model', fallback: 'fallback-model' },
|
||||
storage: { type: 'file' },
|
||||
version: '1.0.0'
|
||||
}),
|
||||
loadLocalConfig: vi.fn().mockResolvedValue(null),
|
||||
loadGlobalConfig: vi.fn().mockResolvedValue(null),
|
||||
hasLocalConfig: vi.fn().mockResolvedValue(false),
|
||||
hasGlobalConfig: vi.fn().mockResolvedValue(false)
|
||||
}) as any
|
||||
);
|
||||
|
||||
vi.mocked(ConfigMerger).mockImplementation(() => ({
|
||||
addSource: vi.fn(),
|
||||
clearSources: vi.fn(),
|
||||
merge: vi.fn().mockReturnValue({
|
||||
models: { main: 'merged-model', fallback: 'fallback-model' },
|
||||
storage: { type: 'file' }
|
||||
}),
|
||||
getSources: vi.fn().mockReturnValue([]),
|
||||
hasSource: vi.fn().mockReturnValue(false),
|
||||
removeSource: vi.fn().mockReturnValue(false)
|
||||
} as any));
|
||||
vi.mocked(ConfigMerger).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
addSource: vi.fn(),
|
||||
clearSources: vi.fn(),
|
||||
merge: vi.fn().mockReturnValue({
|
||||
models: { main: 'merged-model', fallback: 'fallback-model' },
|
||||
storage: { type: 'file' }
|
||||
}),
|
||||
getSources: vi.fn().mockReturnValue([]),
|
||||
hasSource: vi.fn().mockReturnValue(false),
|
||||
removeSource: vi.fn().mockReturnValue(false)
|
||||
}) as any
|
||||
);
|
||||
|
||||
vi.mocked(RuntimeStateManager).mockImplementation(() => ({
|
||||
loadState: vi.fn().mockResolvedValue({ activeTag: 'master' }),
|
||||
saveState: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveTag: vi.fn().mockReturnValue('master'),
|
||||
setActiveTag: vi.fn().mockResolvedValue(undefined),
|
||||
getState: vi.fn().mockReturnValue({ activeTag: 'master' }),
|
||||
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
||||
clearState: vi.fn().mockResolvedValue(undefined)
|
||||
} as any));
|
||||
vi.mocked(RuntimeStateManager).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
loadState: vi.fn().mockResolvedValue({ activeTag: 'master' }),
|
||||
saveState: vi.fn().mockResolvedValue(undefined),
|
||||
getActiveTag: vi.fn().mockReturnValue('master'),
|
||||
setActiveTag: vi.fn().mockResolvedValue(undefined),
|
||||
getState: vi.fn().mockReturnValue({ activeTag: 'master' }),
|
||||
updateMetadata: vi.fn().mockResolvedValue(undefined),
|
||||
clearState: vi.fn().mockResolvedValue(undefined)
|
||||
}) as any
|
||||
);
|
||||
|
||||
vi.mocked(ConfigPersistence).mockImplementation(() => ({
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
configExists: vi.fn().mockResolvedValue(false),
|
||||
deleteConfig: vi.fn().mockResolvedValue(undefined),
|
||||
getBackups: vi.fn().mockResolvedValue([]),
|
||||
restoreFromBackup: vi.fn().mockResolvedValue(undefined)
|
||||
} as any));
|
||||
vi.mocked(ConfigPersistence).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
configExists: vi.fn().mockResolvedValue(false),
|
||||
deleteConfig: vi.fn().mockResolvedValue(undefined),
|
||||
getBackups: vi.fn().mockResolvedValue([]),
|
||||
restoreFromBackup: vi.fn().mockResolvedValue(undefined)
|
||||
}) as any
|
||||
);
|
||||
|
||||
vi.mocked(EnvironmentConfigProvider).mockImplementation(() => ({
|
||||
loadConfig: vi.fn().mockReturnValue({}),
|
||||
getRuntimeState: vi.fn().mockReturnValue({}),
|
||||
hasEnvVar: vi.fn().mockReturnValue(false),
|
||||
getAllTaskmasterEnvVars: vi.fn().mockReturnValue({}),
|
||||
addMapping: vi.fn(),
|
||||
getMappings: vi.fn().mockReturnValue([])
|
||||
} as any));
|
||||
vi.mocked(EnvironmentConfigProvider).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
loadConfig: vi.fn().mockReturnValue({}),
|
||||
getRuntimeState: vi.fn().mockReturnValue({}),
|
||||
hasEnvVar: vi.fn().mockReturnValue(false),
|
||||
getAllTaskmasterEnvVars: vi.fn().mockReturnValue({}),
|
||||
addMapping: vi.fn(),
|
||||
getMappings: vi.fn().mockReturnValue([])
|
||||
}) as any
|
||||
);
|
||||
|
||||
// Since constructor is private, we need to use the factory method
|
||||
// But for testing, we'll create a test instance using create()
|
||||
@@ -167,20 +182,23 @@ describe('ConfigManager', () => {
|
||||
|
||||
it('should return API storage configuration when configured', async () => {
|
||||
// Create a new instance with API storage config
|
||||
vi.mocked(ConfigMerger).mockImplementationOnce(() => ({
|
||||
addSource: vi.fn(),
|
||||
clearSources: vi.fn(),
|
||||
merge: vi.fn().mockReturnValue({
|
||||
storage: {
|
||||
type: 'api',
|
||||
apiEndpoint: 'https://api.example.com',
|
||||
apiAccessToken: 'token123'
|
||||
}
|
||||
}),
|
||||
getSources: vi.fn().mockReturnValue([]),
|
||||
hasSource: vi.fn().mockReturnValue(false),
|
||||
removeSource: vi.fn().mockReturnValue(false)
|
||||
} as any));
|
||||
vi.mocked(ConfigMerger).mockImplementationOnce(
|
||||
() =>
|
||||
({
|
||||
addSource: vi.fn(),
|
||||
clearSources: vi.fn(),
|
||||
merge: vi.fn().mockReturnValue({
|
||||
storage: {
|
||||
type: 'api',
|
||||
apiEndpoint: 'https://api.example.com',
|
||||
apiAccessToken: 'token123'
|
||||
}
|
||||
}),
|
||||
getSources: vi.fn().mockReturnValue([]),
|
||||
hasSource: vi.fn().mockReturnValue(false),
|
||||
removeSource: vi.fn().mockReturnValue(false)
|
||||
}) as any
|
||||
);
|
||||
|
||||
const apiManager = await ConfigManager.create(testProjectRoot);
|
||||
|
||||
@@ -271,7 +289,9 @@ describe('ConfigManager', () => {
|
||||
// Manager is already initialized in the main beforeEach
|
||||
|
||||
it('should update configuration and save', async () => {
|
||||
const updates = { models: { main: 'new-model', fallback: 'fallback-model' } };
|
||||
const updates = {
|
||||
models: { main: 'new-model', fallback: 'fallback-model' }
|
||||
};
|
||||
await manager.updateConfig(updates);
|
||||
|
||||
const persistence = (manager as any).persistence;
|
||||
@@ -302,10 +322,10 @@ describe('ConfigManager', () => {
|
||||
await manager.saveConfig();
|
||||
|
||||
const persistence = (manager as any).persistence;
|
||||
expect(persistence.saveConfig).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{ createBackup: true, atomic: true }
|
||||
);
|
||||
expect(persistence.saveConfig).toHaveBeenCalledWith(expect.any(Object), {
|
||||
createBackup: true,
|
||||
atomic: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,9 +367,11 @@ describe('ConfigManager', () => {
|
||||
|
||||
const unsubscribe = manager.watch(callback);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('Configuration watching not yet implemented');
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Configuration watching not yet implemented'
|
||||
);
|
||||
expect(unsubscribe).toBeInstanceOf(Function);
|
||||
|
||||
|
||||
// Calling unsubscribe should not throw
|
||||
expect(() => unsubscribe()).not.toThrow();
|
||||
|
||||
@@ -364,7 +386,9 @@ describe('ConfigManager', () => {
|
||||
loader.loadLocalConfig.mockRejectedValue(new Error('File error'));
|
||||
|
||||
// Creating a new manager should not throw even if service fails
|
||||
await expect(ConfigManager.create(testProjectRoot)).resolves.not.toThrow();
|
||||
await expect(
|
||||
ConfigManager.create(testProjectRoot)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ConfigManager {
|
||||
/**
|
||||
* Create and initialize a new ConfigManager instance
|
||||
* This is the ONLY way to create a ConfigManager
|
||||
*
|
||||
*
|
||||
* @param projectRoot - The root directory of the project
|
||||
* @returns Fully initialized ConfigManager instance
|
||||
*/
|
||||
|
||||
@@ -34,18 +34,25 @@ export class TaskEntity implements Task {
|
||||
assignee?: string;
|
||||
complexity?: Task['complexity'];
|
||||
|
||||
constructor(data: Task) {
|
||||
constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) {
|
||||
this.validate(data);
|
||||
|
||||
this.id = data.id;
|
||||
// Always convert ID to string
|
||||
this.id = String(data.id);
|
||||
this.title = data.title;
|
||||
this.description = data.description;
|
||||
this.status = data.status;
|
||||
this.priority = data.priority;
|
||||
this.dependencies = data.dependencies || [];
|
||||
// Ensure dependency IDs are also strings
|
||||
this.dependencies = (data.dependencies || []).map((dep) => String(dep));
|
||||
this.details = data.details;
|
||||
this.testStrategy = data.testStrategy;
|
||||
this.subtasks = data.subtasks || [];
|
||||
// Normalize subtask IDs to strings
|
||||
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
||||
...subtask,
|
||||
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
|
||||
parentId: String(subtask.parentId)
|
||||
}));
|
||||
|
||||
// Optional properties
|
||||
this.createdAt = data.createdAt;
|
||||
@@ -60,10 +67,16 @@ export class TaskEntity implements Task {
|
||||
/**
|
||||
* Validate task data
|
||||
*/
|
||||
private validate(data: Partial<Task>): void {
|
||||
if (!data.id || typeof data.id !== 'string') {
|
||||
private validate(
|
||||
data: Partial<Task> | Partial<Omit<Task, 'id'> & { id: number | string }>
|
||||
): void {
|
||||
if (
|
||||
data.id === undefined ||
|
||||
data.id === null ||
|
||||
(typeof data.id !== 'string' && typeof data.id !== 'number')
|
||||
) {
|
||||
throw new TaskMasterError(
|
||||
'Task ID is required and must be a string',
|
||||
'Task ID is required and must be a string or number',
|
||||
ERROR_CODES.VALIDATION_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ export {
|
||||
createTaskMasterCore,
|
||||
type TaskMasterCoreOptions,
|
||||
type ListTasksResult
|
||||
} from './task-master-core.js';
|
||||
} from './task-master-core';
|
||||
|
||||
// Re-export types
|
||||
export type * from './types/index';
|
||||
export type * from './types';
|
||||
|
||||
// Re-export interfaces (types only to avoid conflicts)
|
||||
export type * from './interfaces/index';
|
||||
export type * from './interfaces';
|
||||
|
||||
// Re-export constants
|
||||
export * from './constants/index';
|
||||
export * from './constants';
|
||||
|
||||
// Re-export providers
|
||||
export * from './providers/index';
|
||||
export * from './providers';
|
||||
|
||||
// Re-export storage (selectively to avoid conflicts)
|
||||
export {
|
||||
@@ -29,21 +29,17 @@ export {
|
||||
ApiStorage,
|
||||
StorageFactory,
|
||||
type ApiStorageConfig
|
||||
} from './storage/index';
|
||||
export { PlaceholderStorage, type StorageAdapter } from './storage/index';
|
||||
} from './storage';
|
||||
export { PlaceholderStorage, type StorageAdapter } from './storage';
|
||||
|
||||
// Re-export parser
|
||||
export * from './parser/index';
|
||||
export * from './parser';
|
||||
|
||||
// Re-export utilities
|
||||
export * from './utils/index';
|
||||
export * from './utils';
|
||||
|
||||
// Re-export errors
|
||||
export * from './errors/index';
|
||||
export * from './errors';
|
||||
|
||||
// Re-export entities
|
||||
export { TaskEntity } from './entities/task.entity.js';
|
||||
|
||||
// Package metadata
|
||||
export const version = '1.0.0';
|
||||
export const name = '@task-master/tm-core';
|
||||
export { TaskEntity } from './entities/task.entity';
|
||||
|
||||
@@ -60,9 +60,6 @@ export class TaskService {
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Ensure config manager is initialized
|
||||
await this.configManager.initialize();
|
||||
|
||||
// Create storage based on configuration
|
||||
const storageConfig = this.configManager.getStorageConfig();
|
||||
const projectRoot = this.configManager.getProjectRoot();
|
||||
@@ -83,7 +80,7 @@ export class TaskService {
|
||||
* This is the main method that retrieves tasks from storage and applies filters
|
||||
*/
|
||||
async getTaskList(options: GetTaskListOptions = {}): Promise<TaskListResult> {
|
||||
await this.ensureInitialized();
|
||||
console.log('getTaskList', options);
|
||||
|
||||
// Determine which tag to use
|
||||
const activeTag = this.configManager.getActiveTag();
|
||||
@@ -113,13 +110,15 @@ export class TaskService {
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('tasks', tasks);
|
||||
|
||||
return {
|
||||
tasks,
|
||||
total: rawTasks.length,
|
||||
filtered: filteredEntities.length,
|
||||
tag: options.tag, // Only include tag if explicitly provided
|
||||
storageType: this.configManager.getStorageConfig().type
|
||||
} as TaskListResult;
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to get task list',
|
||||
@@ -336,15 +335,6 @@ export class TaskService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure service is initialized
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current storage type
|
||||
*/
|
||||
|
||||
@@ -127,6 +127,18 @@ export class FileStorage implements IStorage {
|
||||
// Ensure directory exists
|
||||
await this.ensureDirectoryExists();
|
||||
|
||||
// Normalize task IDs to strings (force string IDs everywhere)
|
||||
const normalizedTasks = tasks.map(task => ({
|
||||
...task,
|
||||
id: String(task.id), // Force ID to string
|
||||
dependencies: task.dependencies?.map(dep => String(dep)) || [],
|
||||
subtasks: task.subtasks?.map(subtask => ({
|
||||
...subtask,
|
||||
id: String(subtask.id),
|
||||
parentId: String(subtask.parentId)
|
||||
})) || []
|
||||
}));
|
||||
|
||||
// Check if we need to use legacy format
|
||||
let dataToWrite: any;
|
||||
|
||||
@@ -140,12 +152,12 @@ export class FileStorage implements IStorage {
|
||||
) {
|
||||
dataToWrite = {
|
||||
[resolvedTag]: {
|
||||
tasks,
|
||||
tasks: normalizedTasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount: tasks.filter((t) => t.status === 'done').length,
|
||||
taskCount: normalizedTasks.length,
|
||||
completedCount: normalizedTasks.filter((t) => t.status === 'done').length,
|
||||
tags: [resolvedTag]
|
||||
}
|
||||
}
|
||||
@@ -153,12 +165,12 @@ export class FileStorage implements IStorage {
|
||||
} else {
|
||||
// Use standard format for new files
|
||||
dataToWrite = {
|
||||
tasks,
|
||||
tasks: normalizedTasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount: tasks.filter((t) => t.status === 'done').length,
|
||||
taskCount: normalizedTasks.length,
|
||||
completedCount: normalizedTasks.filter((t) => t.status === 'done').length,
|
||||
tags: tag ? [tag] : []
|
||||
}
|
||||
};
|
||||
@@ -166,12 +178,12 @@ export class FileStorage implements IStorage {
|
||||
} catch (error: any) {
|
||||
// File doesn't exist, use standard format
|
||||
dataToWrite = {
|
||||
tasks,
|
||||
tasks: normalizedTasks,
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date().toISOString(),
|
||||
taskCount: tasks.length,
|
||||
completedCount: tasks.filter((t) => t.status === 'done').length,
|
||||
taskCount: normalizedTasks.length,
|
||||
completedCount: normalizedTasks.filter((t) => t.status === 'done').length,
|
||||
tags: tag ? [tag] : []
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,9 +33,35 @@ export type { GetTaskListOptions } from './services/task-service.js';
|
||||
export class TaskMasterCore {
|
||||
private configManager: ConfigManager;
|
||||
private taskService: TaskService;
|
||||
private initialized = false;
|
||||
|
||||
constructor(options: TaskMasterCoreOptions) {
|
||||
/**
|
||||
* 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<TaskMasterCore> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize by loading services
|
||||
* Private - only called by the factory method
|
||||
*/
|
||||
private async initialize(options: TaskMasterCoreOptions): Promise<void> {
|
||||
if (!options.projectPath) {
|
||||
throw new TaskMasterError(
|
||||
'Project path is required',
|
||||
@@ -43,28 +69,18 @@ export class TaskMasterCore {
|
||||
);
|
||||
}
|
||||
|
||||
// Create config manager
|
||||
this.configManager = new ConfigManager(options.projectPath);
|
||||
|
||||
// Create task service
|
||||
this.taskService = new TaskService(this.configManager);
|
||||
|
||||
// Apply any provided configuration
|
||||
if (options.configuration) {
|
||||
// This will be applied after initialization
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the TaskMasterCore instance
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
try {
|
||||
await this.configManager.initialize();
|
||||
// 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();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
throw new TaskMasterError(
|
||||
'Failed to initialize TaskMasterCore',
|
||||
@@ -75,15 +91,6 @@ export class TaskMasterCore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the instance is initialized
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of tasks with optional filtering
|
||||
* @deprecated Use getTaskList() instead
|
||||
@@ -100,7 +107,6 @@ export class TaskMasterCore {
|
||||
* Get list of tasks with optional filtering
|
||||
*/
|
||||
async getTaskList(options?: GetTaskListOptions): Promise<ListTasksResult> {
|
||||
await this.ensureInitialized();
|
||||
return this.taskService.getTaskList(options);
|
||||
}
|
||||
|
||||
@@ -108,7 +114,6 @@ export class TaskMasterCore {
|
||||
* Get a specific task by ID
|
||||
*/
|
||||
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||
await this.ensureInitialized();
|
||||
return this.taskService.getTask(taskId, tag);
|
||||
}
|
||||
|
||||
@@ -119,7 +124,6 @@ export class TaskMasterCore {
|
||||
status: TaskStatus | TaskStatus[],
|
||||
tag?: string
|
||||
): Promise<Task[]> {
|
||||
await this.ensureInitialized();
|
||||
return this.taskService.getTasksByStatus(status, tag);
|
||||
}
|
||||
|
||||
@@ -132,7 +136,6 @@ export class TaskMasterCore {
|
||||
withSubtasks: number;
|
||||
blocked: number;
|
||||
}> {
|
||||
await this.ensureInitialized();
|
||||
const stats = await this.taskService.getTaskStats(tag);
|
||||
// Remove storageType from the return to maintain backward compatibility
|
||||
const { storageType, ...restStats } = stats;
|
||||
@@ -143,7 +146,6 @@ export class TaskMasterCore {
|
||||
* Get next available task
|
||||
*/
|
||||
async getNextTask(tag?: string): Promise<Task | null> {
|
||||
await this.ensureInitialized();
|
||||
return this.taskService.getNextTask(tag);
|
||||
}
|
||||
|
||||
@@ -173,21 +175,14 @@ export class TaskMasterCore {
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
// TaskService handles storage cleanup internally
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create TaskMasterCore instance
|
||||
*/
|
||||
export function createTaskMasterCore(
|
||||
projectPath: string,
|
||||
options?: {
|
||||
configuration?: Partial<IConfiguration>;
|
||||
}
|
||||
): TaskMasterCore {
|
||||
return new TaskMasterCore({
|
||||
projectPath,
|
||||
configuration: options?.configuration
|
||||
});
|
||||
export async function createTaskMasterCore(
|
||||
options: TaskMasterCoreOptions
|
||||
): Promise<TaskMasterCore> {
|
||||
return TaskMasterCore.create(options);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
shims: true,
|
||||
dts: true,
|
||||
bundle: true, // Bundle everything into one file
|
||||
outDir: 'dist',
|
||||
// Handle TypeScript imports transparently
|
||||
|
||||
Reference in New Issue
Block a user