feat: enhance error display in new commands
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import {
|
||||
displayHeader,
|
||||
displayDashboards,
|
||||
@@ -106,14 +107,7 @@ export class ListTasksCommand extends Command {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
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);
|
||||
displayError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import { displayHeader } from '../ui/index.js';
|
||||
|
||||
@@ -76,12 +77,7 @@ export class NextCommand extends Command {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
message: error?.message ?? String(error)
|
||||
};
|
||||
|
||||
// Allow error to propagate for library compatibility
|
||||
throw new Error(msg.message || 'Unexpected error in next command');
|
||||
displayError(error);
|
||||
} finally {
|
||||
// Always clean up resources, even on error
|
||||
await this.cleanup();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type TaskStatus
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
/**
|
||||
* Valid task status values for validation
|
||||
@@ -135,16 +136,14 @@ export class SetStatusCommand extends Command {
|
||||
oldStatus: result.oldStatus,
|
||||
newStatus: result.newStatus
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(
|
||||
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (options.format === 'json') {
|
||||
const errorMessage = error?.getSanitizedDetails
|
||||
? error.getSanitizedDetails().message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
@@ -153,8 +152,14 @@ export class SetStatusCommand extends Command {
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
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);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,19 +175,17 @@ export class SetStatusCommand extends Command {
|
||||
|
||||
// Display results
|
||||
this.displayResults(this.lastResult, options);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
if (!options.silent) {
|
||||
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
if (options.format === 'json') {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||
process.exit(1);
|
||||
} else if (!options.silent) {
|
||||
displayError(error);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Clean up resources
|
||||
if (this.tmCore) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
|
||||
/**
|
||||
@@ -112,14 +113,7 @@ export class ShowCommand extends Command {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.getSanitizedDetails?.() ?? {
|
||||
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);
|
||||
displayError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@tm/core';
|
||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import { displayError } from '../utils/error-handler.js';
|
||||
|
||||
/**
|
||||
* CLI-specific options interface for the start command
|
||||
@@ -160,8 +161,7 @@ export class StartCommand extends Command {
|
||||
if (spinner) {
|
||||
spinner.fail('Operation failed');
|
||||
}
|
||||
this.handleError(error);
|
||||
process.exit(1);
|
||||
displayError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,22 +452,6 @@ export class StartCommand extends Command {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,9 @@ export {
|
||||
// UI utilities (for other commands to use)
|
||||
export * as ui from './utils/ui.js';
|
||||
|
||||
// Error handling utilities
|
||||
export { displayError, isDebugMode } from './utils/error-handler.js';
|
||||
|
||||
// Auto-update utilities
|
||||
export {
|
||||
checkForUpdate,
|
||||
|
||||
60
apps/cli/src/utils/error-handler.ts
Normal file
60
apps/cli/src/utils/error-handler.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,10 @@ export const ERROR_CODES = {
|
||||
INVALID_INPUT: 'INVALID_INPUT',
|
||||
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
||||
NOT_FOUND: 'NOT_FOUND'
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
|
||||
// Context errors
|
||||
NO_BRIEF_SELECTED: 'NO_BRIEF_SELECTED'
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||
|
||||
@@ -161,6 +161,13 @@ export class TaskService {
|
||||
storageType: this.getStorageType()
|
||||
};
|
||||
} 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);
|
||||
throw new TaskMasterError(
|
||||
'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
|
||||
return await this.storage.loadTask(String(taskId), activeTag);
|
||||
} 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(
|
||||
`Failed to get task ${taskId}`,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
@@ -522,6 +534,11 @@ export class TaskService {
|
||||
activeTag
|
||||
);
|
||||
} 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(
|
||||
`Failed to update task status for ${taskIdStr}`,
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ApiStorage implements IStorage {
|
||||
private async loadTagsIntoCache(): Promise<void> {
|
||||
try {
|
||||
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 (context?.briefId) {
|
||||
@@ -159,12 +159,18 @@ export class ApiStorage implements IStorage {
|
||||
|
||||
try {
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = await authManager.getContext();
|
||||
const context = authManager.getContext();
|
||||
|
||||
// If no brief is selected in context, throw an error
|
||||
if (!context?.briefId) {
|
||||
throw new Error(
|
||||
'No brief selected. Please select a brief first using: tm context brief <brief-id>'
|
||||
throw new TaskMasterError(
|
||||
'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;
|
||||
} 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(
|
||||
'Failed to load tasks from API',
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
@@ -237,10 +251,34 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
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(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
} 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(
|
||||
'Failed to load task from API',
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
@@ -325,7 +363,7 @@ export class ApiStorage implements IStorage {
|
||||
|
||||
try {
|
||||
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
|
||||
if (context?.briefId) {
|
||||
@@ -510,6 +548,22 @@ export class ApiStorage implements IStorage {
|
||||
await this.ensureInitialized();
|
||||
|
||||
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(() =>
|
||||
this.repository.getTask(this.projectId, taskId)
|
||||
);
|
||||
@@ -546,6 +600,14 @@ export class ApiStorage implements IStorage {
|
||||
taskId
|
||||
};
|
||||
} 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(
|
||||
'Failed to update task status via API',
|
||||
ERROR_CODES.STORAGE_ERROR,
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
registerAllCommands,
|
||||
checkForUpdate,
|
||||
performAutoUpdate,
|
||||
displayUpgradeNotification
|
||||
displayUpgradeNotification,
|
||||
displayError
|
||||
} from '@tm/cli';
|
||||
|
||||
import {
|
||||
@@ -5156,10 +5157,7 @@ async function runCLI(argv = process.argv) {
|
||||
);
|
||||
} else {
|
||||
// Generic error handling for other errors
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
if (getDebugFlag()) {
|
||||
console.error(error);
|
||||
}
|
||||
displayError(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user