chore: add logger module inside tm-core

This commit is contained in:
Ralph Khreish
2025-09-02 15:54:55 +02:00
parent 6409e1a366
commit a03a0cb45a
12 changed files with 397 additions and 7050 deletions

34
package-lock.json generated
View File

@@ -95,6 +95,7 @@
"license": "MIT",
"dependencies": {
"@tm/core": "*",
"@tm/logger": "*",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"cli-table3": "^0.6.5",
@@ -9058,6 +9059,10 @@
"resolved": "packages/tm-core",
"link": true
},
"node_modules/@tm/logger": {
"resolved": "packages/logger",
"link": true
},
"node_modules/@tokenizer/inflate": {
"version": "0.2.7",
"license": "MIT",
@@ -26153,11 +26158,40 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/logger": {
"name": "@tm/logger",
"version": "1.0.0",
"dependencies": {
"chalk": "^5.3.0"
},
"devDependencies": {
"@types/node": "^20.11.5",
"typescript": "^5.3.3"
}
},
"packages/logger/node_modules/@types/node": {
"version": "20.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"packages/logger/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"packages/tm-core": {
"name": "@tm/core",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@tm/logger": "*",
"zod": "^3.22.4"
},
"devDependencies": {

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"chalk": "^5.3.0",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -9,6 +9,7 @@ import https from 'https';
import http from 'http';
import { URL } from 'url';
import crypto from 'crypto';
import { getLogger } from '../logger';
// Auth configuration
const AUTH_CONFIG_DIR = path.join(os.homedir(), '.taskmaster');
@@ -58,6 +59,7 @@ export class AuthenticationError extends Error {
*/
export class AuthManager {
private static instance: AuthManager;
private logger = getLogger('AuthManager');
private constructor() {}
@@ -98,13 +100,13 @@ export class AuthManager {
// Check if token is expired
if (authData.expiresAt && new Date(authData.expiresAt) < new Date()) {
console.warn('Authentication token has expired');
this.logger.warn('Authentication token has expired');
return null;
}
return authData;
} catch (error) {
console.error(
this.logger.error(
`Failed to read auth credentials: ${(error as Error).message}`
);
return null;

View File

@@ -135,25 +135,24 @@ export class ConfigManager {
* Get storage configuration
*/
getStorageConfig(): {
type: 'file' | 'api';
type: 'file' | 'api' | 'auto';
apiEndpoint?: string;
apiAccessToken?: string;
} {
const storage = this.config.storage;
if (
storage?.type === 'api' &&
storage.apiEndpoint &&
storage.apiAccessToken
) {
// Return the configured type (including 'auto')
const storageType = storage?.type || 'auto';
if (storageType === 'api' || storageType === 'auto') {
return {
type: 'api',
apiEndpoint: storage.apiEndpoint,
apiAccessToken: storage.apiAccessToken
type: storageType,
apiEndpoint: storage?.apiEndpoint,
apiAccessToken: storage?.apiAccessToken
};
}
return { type: 'file' };
return { type: storageType };
}
/**
@@ -269,12 +268,4 @@ export class ConfigManager {
getConfigSources() {
return this.merger.getSources();
}
/**
* Watch for configuration changes (placeholder for future)
*/
watch(_callback: (config: PartialConfiguration) => void): () => void {
console.warn('Configuration watching not yet implemented');
return () => {}; // Return no-op unsubscribe function
}
}

View File

@@ -77,8 +77,8 @@ export interface TagSettings {
* Storage and persistence settings
*/
export interface StorageSettings {
/** Storage backend type */
type: 'file' | 'api';
/** Storage backend type - 'auto' detects based on auth status */
type: 'file' | 'api' | 'auto';
/** Base path for file storage */
basePath?: string;
/** API endpoint for API storage (Hamster integration) */

View File

@@ -0,0 +1,56 @@
/**
* @fileoverview Logger factory and singleton management
*/
import { Logger, LoggerConfig } from './logger.js';
// Global logger instance
let globalLogger: Logger | null = null;
// Named logger instances
const loggers = new Map<string, Logger>();
/**
* Create a new logger instance
*/
export function createLogger(config?: LoggerConfig): Logger {
return new Logger(config);
}
/**
* Get or create a named logger instance
*/
export function getLogger(name?: string, config?: LoggerConfig): Logger {
// If no name provided, return global logger
if (!name) {
if (!globalLogger) {
globalLogger = createLogger(config);
}
return globalLogger;
}
// Check if named logger exists
if (!loggers.has(name)) {
loggers.set(name, createLogger({
prefix: name,
...config
}));
}
return loggers.get(name)!;
}
/**
* Set the global logger instance
*/
export function setGlobalLogger(logger: Logger): void {
globalLogger = logger;
}
/**
* Clear all logger instances (useful for testing)
*/
export function clearLoggers(): void {
globalLogger = null;
loggers.clear();
}

View File

@@ -0,0 +1,8 @@
/**
* @fileoverview Logger package for Task Master
* Provides centralized logging with support for different modes and levels
*/
export { Logger, LogLevel } from './logger.js';
export type { LoggerConfig } from './logger.js';
export { createLogger, getLogger, setGlobalLogger } from './factory.js';

View File

@@ -0,0 +1,217 @@
/**
* @fileoverview Core logger implementation
*/
import chalk from 'chalk';
export enum LogLevel {
SILENT = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4
}
export interface LoggerConfig {
level?: LogLevel;
silent?: boolean;
prefix?: string;
timestamp?: boolean;
colors?: boolean;
// MCP mode silences all output
mcpMode?: boolean;
}
export class Logger {
private config: Required<LoggerConfig>;
private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = {
level: LogLevel.INFO,
silent: false,
prefix: '',
timestamp: false,
colors: true,
mcpMode: false
};
constructor(config: LoggerConfig = {}) {
// Check environment variables
const envConfig: LoggerConfig = {};
// Check for MCP mode
if (process.env.MCP_MODE === 'true' || process.env.TASK_MASTER_MCP === 'true') {
envConfig.mcpMode = true;
}
// Check for silent mode
if (process.env.TASK_MASTER_SILENT === 'true' || process.env.TM_SILENT === 'true') {
envConfig.silent = true;
}
// Check for log level
if (process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL) {
const levelStr = (process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL || '').toUpperCase();
if (levelStr in LogLevel) {
envConfig.level = LogLevel[levelStr as keyof typeof LogLevel];
}
}
// Check for no colors
if (process.env.NO_COLOR === 'true' || process.env.TASK_MASTER_NO_COLOR === 'true') {
envConfig.colors = false;
}
// Merge configs: defaults < constructor < environment
this.config = {
...Logger.DEFAULT_CONFIG,
...config,
...envConfig
};
// MCP mode overrides everything to be silent
if (this.config.mcpMode) {
this.config.silent = true;
}
}
/**
* Check if logging is enabled for a given level
*/
private shouldLog(level: LogLevel): boolean {
if (this.config.silent || this.config.mcpMode) {
return false;
}
return level <= this.config.level;
}
/**
* Format a log message
*/
private formatMessage(level: LogLevel, message: string, ...args: any[]): string {
let formatted = '';
// Add timestamp if enabled
if (this.config.timestamp) {
const timestamp = new Date().toISOString();
formatted += this.config.colors ? chalk.gray(`[${timestamp}] `) : `[${timestamp}] `;
}
// Add prefix if configured
if (this.config.prefix) {
formatted += this.config.colors ? chalk.cyan(`[${this.config.prefix}] `) : `[${this.config.prefix}] `;
}
// Skip level indicator for cleaner output
// We can still color the message based on level
if (this.config.colors) {
switch (level) {
case LogLevel.ERROR:
message = chalk.red(message);
break;
case LogLevel.WARN:
message = chalk.yellow(message);
break;
case LogLevel.INFO:
// Info stays default color
break;
case LogLevel.DEBUG:
message = chalk.gray(message);
break;
}
}
// Add the message
formatted += message;
// Add any additional arguments
if (args.length > 0) {
formatted += ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' ');
}
return formatted;
}
/**
* Log an error message
*/
error(message: string, ...args: any[]): void {
if (!this.shouldLog(LogLevel.ERROR)) return;
console.error(this.formatMessage(LogLevel.ERROR, message, ...args));
}
/**
* Log a warning message
*/
warn(message: string, ...args: any[]): void {
if (!this.shouldLog(LogLevel.WARN)) return;
console.warn(this.formatMessage(LogLevel.WARN, message, ...args));
}
/**
* Log an info message
*/
info(message: string, ...args: any[]): void {
if (!this.shouldLog(LogLevel.INFO)) return;
console.log(this.formatMessage(LogLevel.INFO, message, ...args));
}
/**
* Log a debug message
*/
debug(message: string, ...args: any[]): void {
if (!this.shouldLog(LogLevel.DEBUG)) return;
console.log(this.formatMessage(LogLevel.DEBUG, message, ...args));
}
/**
* Log a message without any formatting (raw output)
* Useful for CLI output that should appear as-is
*/
log(message: string, ...args: any[]): void {
if (this.config.silent || this.config.mcpMode) return;
if (args.length > 0) {
console.log(message, ...args);
} else {
console.log(message);
}
}
/**
* Update logger configuration
*/
setConfig(config: Partial<LoggerConfig>): void {
this.config = {
...this.config,
...config
};
// MCP mode always overrides to silent
if (this.config.mcpMode) {
this.config.silent = true;
}
}
/**
* Get current configuration
*/
getConfig(): Readonly<Required<LoggerConfig>> {
return { ...this.config };
}
/**
* Create a child logger with a prefix
*/
child(prefix: string, config?: Partial<LoggerConfig>): Logger {
const childPrefix = this.config.prefix
? `${this.config.prefix}:${prefix}`
: prefix;
return new Logger({
...this.config,
...config,
prefix: childPrefix
});
}
}

View File

@@ -22,8 +22,8 @@ export interface TaskListResult {
filtered: number;
/** The tag these tasks belong to (only present if explicitly provided) */
tag?: string;
/** Storage type being used */
storageType: 'file' | 'api';
/** Storage type being used - includes 'auto' for automatic detection */
storageType: 'file' | 'api' | 'auto';
}
/**
@@ -166,7 +166,7 @@ export class TaskService {
byStatus: Record<TaskStatus, number>;
withSubtasks: number;
blocked: number;
storageType: 'file' | 'api';
storageType: 'file' | 'api' | 'auto';
}> {
const result = await this.getTaskList({
tag,
@@ -334,7 +334,7 @@ export class TaskService {
/**
* Get current storage type
*/
getStorageType(): 'file' | 'api' {
getStorageType(): 'file' | 'api' | 'auto' {
return this.configManager.getStorageConfig().type;
}

View File

@@ -7,6 +7,8 @@ import type { IConfiguration } from '../interfaces/configuration.interface.js';
import { FileStorage } from './file-storage';
import { ApiStorage } from './api-storage.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { AuthManager } from '../auth/auth-manager.js';
import { getLogger } from '../logger/index.js';
/**
* Factory for creating storage implementations based on configuration
@@ -22,15 +24,71 @@ export class StorageFactory {
config: Partial<IConfiguration>,
projectPath: string
): IStorage {
const storageType = config.storage?.type || 'file';
const storageType = config.storage?.type || 'auto';
const logger = getLogger('StorageFactory');
switch (storageType) {
case 'file':
logger.debug('📁 Using local file storage');
return StorageFactory.createFileStorage(projectPath, config);
case 'api':
if (!StorageFactory.isHamsterAvailable(config)) {
// Check if authenticated via AuthManager
const authManager = AuthManager.getInstance();
if (!authManager.isAuthenticated()) {
throw new TaskMasterError(
'API storage configured but not authenticated. Run: tm auth login',
ERROR_CODES.MISSING_CONFIGURATION,
{ storageType: 'api' }
);
}
// Use auth token from AuthManager
const credentials = authManager.getCredentials();
if (credentials) {
// Merge with existing storage config, ensuring required fields
config.storage = {
...config.storage,
type: 'api' as const,
apiAccessToken: credentials.token,
apiEndpoint: config.storage?.apiEndpoint || process.env.HAMSTER_API_URL || 'https://tryhamster.com/api'
} as any; // Cast to any to bypass strict type checking for partial config
}
}
logger.info('☁️ Using API storage');
return StorageFactory.createApiStorage(config);
case 'auto':
// Auto-detect based on authentication status
const authManager = AuthManager.getInstance();
// First check if API credentials are explicitly configured
if (StorageFactory.isHamsterAvailable(config)) {
logger.info('☁️ Using API storage (configured)');
return StorageFactory.createApiStorage(config);
}
// Then check if authenticated via AuthManager
if (authManager.isAuthenticated()) {
const credentials = authManager.getCredentials();
if (credentials) {
// Configure API storage with auth credentials
config.storage = {
...config.storage,
type: 'api' as const,
apiAccessToken: credentials.token,
apiEndpoint: config.storage?.apiEndpoint || process.env.HAMSTER_API_URL || 'https://tryhamster.com/api'
} as any; // Cast to any to bypass strict type checking for partial config
logger.info('☁️ Using API storage (authenticated)');
return StorageFactory.createApiStorage(config);
}
}
// Default to file storage
logger.debug('📁 Using local file storage');
return StorageFactory.createFileStorage(projectPath, config);
default:
throw new TaskMasterError(
`Unknown storage type: ${storageType}`,
@@ -157,7 +215,8 @@ export class StorageFactory {
await apiStorage.initialize();
return apiStorage;
} catch (error) {
console.warn(
const logger = getLogger('StorageFactory');
logger.warn(
'Failed to initialize API storage, falling back to file storage:',
error
);

View File

@@ -152,7 +152,7 @@ export class TaskMasterCore {
/**
* Get current storage type
*/
getStorageType(): 'file' | 'api' {
getStorageType(): 'file' | 'api' | 'auto' {
return this.taskService.getStorageType();
}