chore: add logger module inside tm-core
This commit is contained in:
34
package-lock.json
generated
34
package-lock.json
generated
@@ -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": {
|
||||
|
||||
7021
packages/tm-core/package-lock.json
generated
7021
packages/tm-core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) */
|
||||
|
||||
56
packages/tm-core/src/logger/factory.ts
Normal file
56
packages/tm-core/src/logger/factory.ts
Normal 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();
|
||||
}
|
||||
8
packages/tm-core/src/logger/index.ts
Normal file
8
packages/tm-core/src/logger/index.ts
Normal 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';
|
||||
217
packages/tm-core/src/logger/logger.ts
Normal file
217
packages/tm-core/src/logger/logger.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -152,7 +152,7 @@ export class TaskMasterCore {
|
||||
/**
|
||||
* Get current storage type
|
||||
*/
|
||||
getStorageType(): 'file' | 'api' {
|
||||
getStorageType(): 'file' | 'api' | 'auto' {
|
||||
return this.taskService.getStorageType();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user