feat: enhance error display in new commands

This commit is contained in:
Ralph Khreish
2025-10-16 13:56:32 +02:00
parent 8649c8a347
commit 4628f5179c
11 changed files with 185 additions and 71 deletions

View File

@@ -17,6 +17,7 @@ import {
} from '@tm/core'; } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
import { import {
displayHeader, displayHeader,
displayDashboards, displayDashboards,
@@ -106,14 +107,7 @@ export class ListTasksCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? { displayError(error);
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
} }
} }

View File

@@ -9,6 +9,7 @@ import chalk from 'chalk';
import boxen from 'boxen'; import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core/types';
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 { displayHeader } from '../ui/index.js'; import { displayHeader } from '../ui/index.js';
@@ -76,12 +77,7 @@ export class NextCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? { displayError(error);
message: error?.message ?? String(error)
};
// Allow error to propagate for library compatibility
throw new Error(msg.message || 'Unexpected error in next command');
} finally { } finally {
// Always clean up resources, even on error // Always clean up resources, even on error
await this.cleanup(); await this.cleanup();

View File

@@ -12,6 +12,7 @@ import {
type TaskStatus type TaskStatus
} from '@tm/core'; } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core/types';
import { displayError } from '../utils/error-handler.js';
/** /**
* Valid task status values for validation * Valid task status values for validation
@@ -135,16 +136,14 @@ export class SetStatusCommand extends Command {
oldStatus: result.oldStatus, oldStatus: result.oldStatus,
newStatus: result.newStatus newStatus: result.newStatus
}); });
} catch (error) { } catch (error: any) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (!options.silent) {
console.error(
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
);
}
if (options.format === 'json') { if (options.format === 'json') {
const errorMessage = error?.getSanitizedDetails
? error.getSanitizedDetails().message
: error instanceof Error
? error.message
: String(error);
console.log( console.log(
JSON.stringify({ JSON.stringify({
success: false, success: false,
@@ -153,8 +152,14 @@ export class SetStatusCommand extends Command {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}) })
); );
}
process.exit(1); process.exit(1);
} else if (!options.silent) {
// Show which task failed with context
console.error(chalk.red(`\nFailed to update task ${taskId}:`));
displayError(error);
} else {
process.exit(1);
}
} }
} }
@@ -170,19 +175,17 @@ export class SetStatusCommand extends Command {
// Display results // Display results
this.displayResults(this.lastResult, options); this.displayResults(this.lastResult, options);
} catch (error) { } catch (error: any) {
if (options.format === 'json') {
const errorMessage = const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred'; error instanceof Error ? error.message : 'Unknown error occurred';
if (!options.silent) {
console.error(chalk.red(`Error: ${errorMessage}`));
}
if (options.format === 'json') {
console.log(JSON.stringify({ success: false, error: errorMessage })); console.log(JSON.stringify({ success: false, error: errorMessage }));
}
process.exit(1); process.exit(1);
} else if (!options.silent) {
displayError(error);
} else {
process.exit(1);
}
} finally { } finally {
// Clean up resources // Clean up resources
if (this.tmCore) { if (this.tmCore) {

View File

@@ -9,6 +9,7 @@ import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core'; import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types'; import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.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';
/** /**
@@ -112,14 +113,7 @@ export class ShowCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? { displayError(error);
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
} }
} }

View File

@@ -16,6 +16,7 @@ import {
} from '@tm/core'; } from '@tm/core';
import { displayTaskDetails } from '../ui/components/task-detail.component.js'; import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/** /**
* CLI-specific options interface for the start command * CLI-specific options interface for the start command
@@ -160,8 +161,7 @@ export class StartCommand extends Command {
if (spinner) { if (spinner) {
spinner.fail('Operation failed'); spinner.fail('Operation failed');
} }
this.handleError(error); displayError(error);
process.exit(1);
} }
} }
@@ -452,22 +452,6 @@ export class StartCommand extends Command {
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`); console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
} }
/**
* Handle general errors
*/
private handleError(error: any): void {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
// Show stack trace in development mode or when DEBUG is set
const isDevelopment = process.env.NODE_ENV !== 'production';
if ((isDevelopment || process.env.DEBUG) && error.stack) {
console.error(chalk.gray(error.stack));
}
}
/** /**
* Set the last result for programmatic access * Set the last result for programmatic access
*/ */

View File

@@ -24,6 +24,9 @@ export {
// UI utilities (for other commands to use) // UI utilities (for other commands to use)
export * as ui from './utils/ui.js'; export * as ui from './utils/ui.js';
// Error handling utilities
export { displayError, isDebugMode } from './utils/error-handler.js';
// Auto-update utilities // Auto-update utilities
export { export {
checkForUpdate, checkForUpdate,

View File

@@ -0,0 +1,60 @@
/**
* @fileoverview Centralized error handling utilities for CLI
* Provides consistent error formatting and debug mode detection
*/
import chalk from 'chalk';
/**
* Check if debug mode is enabled via environment variable
* Only returns true when DEBUG is explicitly set to 'true' or '1'
*
* @returns True if debug mode is enabled
*/
export function isDebugMode(): boolean {
return process.env.DEBUG === 'true' || process.env.DEBUG === '1';
}
/**
* Display an error to the user with optional stack trace in debug mode
* Handles both TaskMasterError instances and regular errors
*
* @param error - The error to display
* @param options - Display options
*/
export function displayError(
error: any,
options: {
/** Skip exit, useful when caller wants to handle exit */
skipExit?: boolean;
/** Force show stack trace regardless of debug mode */
forceStack?: boolean;
} = {}
): void {
// Check if it's a TaskMasterError with sanitized details
if (error?.getSanitizedDetails) {
const sanitized = error.getSanitizedDetails();
console.error(chalk.red(`\n${sanitized.message}`));
// Show stack trace in debug mode or if forced
if ((isDebugMode() || options.forceStack) && error.stack) {
console.error(chalk.gray('\nStack trace:'));
console.error(chalk.gray(error.stack));
}
} else {
// For other errors, show the message
const message = error?.message ?? String(error);
console.error(chalk.red(`\nError: ${message}`));
// Show stack trace in debug mode or if forced
if ((isDebugMode() || options.forceStack) && error?.stack) {
console.error(chalk.gray('\nStack trace:'));
console.error(chalk.gray(error.stack));
}
}
// Exit if not skipped
if (!options.skipExit) {
process.exit(1);
}
}

View File

@@ -52,7 +52,10 @@ export const ERROR_CODES = {
INVALID_INPUT: 'INVALID_INPUT', INVALID_INPUT: 'INVALID_INPUT',
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED', NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
UNKNOWN_ERROR: 'UNKNOWN_ERROR', UNKNOWN_ERROR: 'UNKNOWN_ERROR',
NOT_FOUND: 'NOT_FOUND' NOT_FOUND: 'NOT_FOUND',
// Context errors
NO_BRIEF_SELECTED: 'NO_BRIEF_SELECTED'
} as const; } as const;
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];

View File

@@ -161,6 +161,13 @@ export class TaskService {
storageType: this.getStorageType() storageType: this.getStorageType()
}; };
} catch (error) { } catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
if (error instanceof TaskMasterError && error.is(ERROR_CODES.NO_BRIEF_SELECTED)) {
// Just re-throw user-facing errors without wrapping
throw error;
}
// Log internal errors
this.logger.error('Failed to get task list', error); this.logger.error('Failed to get task list', error);
throw new TaskMasterError( throw new TaskMasterError(
'Failed to get task list', 'Failed to get task list',
@@ -186,6 +193,11 @@ export class TaskService {
// Delegate to storage layer which handles the specific logic for tasks vs subtasks // Delegate to storage layer which handles the specific logic for tasks vs subtasks
return await this.storage.loadTask(String(taskId), activeTag); return await this.storage.loadTask(String(taskId), activeTag);
} catch (error) { } catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (error instanceof TaskMasterError && error.is(ERROR_CODES.NO_BRIEF_SELECTED)) {
throw error;
}
throw new TaskMasterError( throw new TaskMasterError(
`Failed to get task ${taskId}`, `Failed to get task ${taskId}`,
ERROR_CODES.STORAGE_ERROR, ERROR_CODES.STORAGE_ERROR,
@@ -522,6 +534,11 @@ export class TaskService {
activeTag activeTag
); );
} catch (error) { } catch (error) {
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
if (error instanceof TaskMasterError && error.is(ERROR_CODES.NO_BRIEF_SELECTED)) {
throw error;
}
throw new TaskMasterError( throw new TaskMasterError(
`Failed to update task status for ${taskIdStr}`, `Failed to update task status for ${taskIdStr}`,
ERROR_CODES.STORAGE_ERROR, ERROR_CODES.STORAGE_ERROR,

View File

@@ -126,7 +126,7 @@ export class ApiStorage implements IStorage {
private async loadTagsIntoCache(): Promise<void> { private async loadTagsIntoCache(): Promise<void> {
try { try {
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const context = await authManager.getContext(); const context = authManager.getContext();
// If we have a selected brief, create a virtual "tag" for it // If we have a selected brief, create a virtual "tag" for it
if (context?.briefId) { if (context?.briefId) {
@@ -159,12 +159,18 @@ export class ApiStorage implements IStorage {
try { try {
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const context = await authManager.getContext(); const context = authManager.getContext();
// If no brief is selected in context, throw an error // If no brief is selected in context, throw an error
if (!context?.briefId) { if (!context?.briefId) {
throw new Error( throw new TaskMasterError(
'No brief selected. Please select a brief first using: tm context brief <brief-id>' 'No brief selected',
ERROR_CODES.NO_BRIEF_SELECTED,
{
operation: 'loadTasks',
userMessage:
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
}
); );
} }
@@ -181,6 +187,14 @@ export class ApiStorage implements IStorage {
return tasks; return tasks;
} catch (error) { } catch (error) {
// If it's already a NO_BRIEF_SELECTED error, don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
throw error;
}
throw new TaskMasterError( throw new TaskMasterError(
'Failed to load tasks from API', 'Failed to load tasks from API',
ERROR_CODES.STORAGE_ERROR, ERROR_CODES.STORAGE_ERROR,
@@ -237,10 +251,34 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
// If no brief is selected in context, throw an error
if (!context?.briefId) {
throw new TaskMasterError(
'No brief selected',
ERROR_CODES.NO_BRIEF_SELECTED,
{
operation: 'loadTask',
userMessage:
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
}
);
}
return await this.retryOperation(() => return await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId) this.repository.getTask(this.projectId, taskId)
); );
} catch (error) { } catch (error) {
// If it's already a NO_BRIEF_SELECTED error, don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
throw error;
}
throw new TaskMasterError( throw new TaskMasterError(
'Failed to load task from API', 'Failed to load task from API',
ERROR_CODES.STORAGE_ERROR, ERROR_CODES.STORAGE_ERROR,
@@ -325,7 +363,7 @@ export class ApiStorage implements IStorage {
try { try {
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const context = await authManager.getContext(); const context = authManager.getContext();
// In our API-based system, we only have one "tag" at a time - the current brief // In our API-based system, we only have one "tag" at a time - the current brief
if (context?.briefId) { if (context?.briefId) {
@@ -510,6 +548,22 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
// If no brief is selected in context, throw an error
if (!context?.briefId) {
throw new TaskMasterError(
'No brief selected',
ERROR_CODES.NO_BRIEF_SELECTED,
{
operation: 'updateTaskStatus',
userMessage:
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
}
);
}
const existingTask = await this.retryOperation(() => const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId) this.repository.getTask(this.projectId, taskId)
); );
@@ -546,6 +600,14 @@ export class ApiStorage implements IStorage {
taskId taskId
}; };
} catch (error) { } catch (error) {
// If it's already a NO_BRIEF_SELECTED error, don't wrap it
if (
error instanceof TaskMasterError &&
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
) {
throw error;
}
throw new TaskMasterError( throw new TaskMasterError(
'Failed to update task status via API', 'Failed to update task status via API',
ERROR_CODES.STORAGE_ERROR, ERROR_CODES.STORAGE_ERROR,

View File

@@ -19,7 +19,8 @@ import {
registerAllCommands, registerAllCommands,
checkForUpdate, checkForUpdate,
performAutoUpdate, performAutoUpdate,
displayUpgradeNotification displayUpgradeNotification,
displayError
} from '@tm/cli'; } from '@tm/cli';
import { import {
@@ -5156,10 +5157,7 @@ async function runCLI(argv = process.argv) {
); );
} else { } else {
// Generic error handling for other errors // Generic error handling for other errors
console.error(chalk.red(`Error: ${error.message}`)); displayError(error);
if (getDebugFlag()) {
console.error(error);
}
} }
process.exit(1); process.exit(1);