diff --git a/apps/cli/src/commands/next.command.ts b/apps/cli/src/commands/next.command.ts index 64ade64f..50b37169 100644 --- a/apps/cli/src/commands/next.command.ts +++ b/apps/cli/src/commands/next.command.ts @@ -59,6 +59,7 @@ export class NextCommand extends Command { * Execute the next command */ private async executeCommand(options: NextCommandOptions): Promise { + let hasError = false; try { // Validate options (throws on invalid options) this.validateOptions(options); @@ -77,11 +78,17 @@ export class NextCommand extends Command { this.displayResults(result, options); } } catch (error: any) { - displayError(error); + hasError = true; + displayError(error, { skipExit: true }); } finally { // Always clean up resources, even on error await this.cleanup(); } + + // Exit after cleanup completes + if (hasError) { + process.exit(1); + } } /** diff --git a/apps/cli/src/commands/set-status.command.ts b/apps/cli/src/commands/set-status.command.ts index b0a591e8..9e08b1cd 100644 --- a/apps/cli/src/commands/set-status.command.ts +++ b/apps/cli/src/commands/set-status.command.ts @@ -86,6 +86,7 @@ export class SetStatusCommand extends Command { private async executeCommand( options: SetStatusCommandOptions ): Promise { + let hasError = false; try { // Validate required options if (!options.id) { @@ -137,6 +138,7 @@ export class SetStatusCommand extends Command { newStatus: result.newStatus }); } catch (error: any) { + hasError = true; if (options.format === 'json') { const errorMessage = error?.getSanitizedDetails ? error.getSanitizedDetails().message @@ -152,14 +154,13 @@ 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); + displayError(error, { skipExit: true }); } + // Don't exit here - let finally block clean up first + break; } } @@ -176,15 +177,13 @@ export class SetStatusCommand extends Command { // Display results this.displayResults(this.lastResult, options); } catch (error: any) { + hasError = true; 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); + displayError(error, { skipExit: true }); } } finally { // Clean up resources @@ -192,6 +191,11 @@ export class SetStatusCommand extends Command { await this.tmCore.close(); } } + + // Exit after cleanup completes + if (hasError) { + process.exit(1); + } } /** diff --git a/apps/cli/src/utils/display-helpers.ts b/apps/cli/src/utils/display-helpers.ts index 0e731f04..acbce710 100644 --- a/apps/cli/src/utils/display-helpers.ts +++ b/apps/cli/src/utils/display-helpers.ts @@ -58,6 +58,8 @@ export function displayCommandHeader( } // Get file path for display (only for file storage) + // Note: The file structure is fixed for file storage and won't change. + // This is a display-only relative path, not used for actual file operations. const filePath = storageType === 'file' && tmCore ? `.taskmaster/tasks/tasks.json` diff --git a/packages/tm-core/src/storage/api-storage.ts b/packages/tm-core/src/storage/api-storage.ts index d7abf663..b6651da1 100644 --- a/packages/tm-core/src/storage/api-storage.ts +++ b/packages/tm-core/src/storage/api-storage.ts @@ -158,21 +158,7 @@ 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: 'loadTasks', - userMessage: - 'No brief selected. Please select a brief first using: tm context brief or tm context brief ' - } - ); - } + const context = this.ensureBriefSelected('loadTasks'); // Load tasks from the current brief context with filters pushed to repository const tasks = await this.retryOperation(() => @@ -187,20 +173,11 @@ 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, - { operation: 'loadTasks', tag, context: 'brief-based loading' }, - error as Error - ); + this.wrapError(error, 'Failed to load tasks from API', { + operation: 'loadTasks', + tag, + context: 'brief-based loading' + }); } } @@ -251,40 +228,17 @@ 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 or tm context brief ' - } - ); - } + this.ensureBriefSelected('loadTask'); 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, - { operation: 'loadTask', taskId, tag }, - error as Error - ); + this.wrapError(error, 'Failed to load task from API', { + operation: 'loadTask', + taskId, + tag + }); } } @@ -548,21 +502,7 @@ 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 or tm context brief ' - } - ); - } + this.ensureBriefSelected('updateTaskStatus'); const existingTask = await this.retryOperation(() => this.repository.getTask(this.projectId, taskId) @@ -600,20 +540,12 @@ 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, - { operation: 'updateTaskStatus', taskId, newStatus, tag }, - error as Error - ); + this.wrapError(error, 'Failed to update task status via API', { + operation: 'updateTaskStatus', + taskId, + newStatus, + tag + }); } } @@ -831,6 +763,29 @@ export class ApiStorage implements IStorage { } } + /** + * Ensure a brief is selected in the current context + * @returns The current auth context with a valid briefId + */ + private ensureBriefSelected(operation: string) { + const authManager = AuthManager.getInstance(); + const context = authManager.getContext(); + + if (!context?.briefId) { + throw new TaskMasterError( + 'No brief selected', + ERROR_CODES.NO_BRIEF_SELECTED, + { + operation, + userMessage: + 'No brief selected. Please select a brief first using: tm context brief or tm context brief ' + } + ); + } + + return context; + } + /** * Retry an operation with exponential backoff */ @@ -849,4 +804,28 @@ export class ApiStorage implements IStorage { throw error; } } + + /** + * Wrap an error unless it's already a NO_BRIEF_SELECTED error + */ + private wrapError( + error: unknown, + message: string, + context: Record + ): never { + // 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( + message, + ERROR_CODES.STORAGE_ERROR, + context, + error as Error + ); + } }