Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
1c5d5651a9 docs: auto-update documentation based on changes in next branch
This PR was automatically generated to update documentation based on recent changes.

  Original commit: chore: apply requested coderabbit changes\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-10-16 17:31:18 +00:00
21 changed files with 322 additions and 421 deletions

View File

@@ -15,7 +15,6 @@ import {
} from '@tm/core/auth'; } from '@tm/core/auth';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { ContextCommand } from './context.command.js'; import { ContextCommand } from './context.command.js';
import { displayError } from '../utils/error-handler.js';
/** /**
* Result type from auth command * Result type from auth command
@@ -118,7 +117,8 @@ export class AuthCommand extends Command {
process.exit(0); process.exit(0);
}, 100); }, 100);
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -134,7 +134,8 @@ export class AuthCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -146,7 +147,8 @@ export class AuthCommand extends Command {
const result = this.displayStatus(); const result = this.displayStatus();
this.setLastResult(result); this.setLastResult(result);
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -162,7 +164,8 @@ export class AuthCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -387,7 +390,7 @@ export class AuthCommand extends Command {
message: 'Authentication successful' message: 'Authentication successful'
}; };
} catch (error) { } catch (error) {
displayError(error, { skipExit: true }); this.handleAuthError(error as AuthenticationError);
return { return {
success: false, success: false,
@@ -450,6 +453,51 @@ export class AuthCommand extends Command {
} }
} }
/**
* Handle authentication errors
*/
private handleAuthError(error: AuthenticationError): void {
console.error(chalk.red(`\n✗ ${error.message}`));
switch (error.code) {
case 'NETWORK_ERROR':
ui.displayWarning(
'Please check your internet connection and try again.'
);
break;
case 'INVALID_CREDENTIALS':
ui.displayWarning('Please check your credentials and try again.');
break;
case 'AUTH_EXPIRED':
ui.displayWarning(
'Your session has expired. Please authenticate again.'
);
break;
default:
if (process.env.DEBUG) {
console.error(chalk.gray(error.stack || ''));
}
}
}
/**
* Handle general errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
this.handleAuthError(error);
} else {
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));
}
}
}
/** /**
* Set the last result for programmatic access * Set the last result for programmatic access
*/ */

View File

@@ -8,9 +8,12 @@ import chalk from 'chalk';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import search from '@inquirer/search'; import search from '@inquirer/search';
import ora, { Ora } from 'ora'; import ora, { Ora } from 'ora';
import { AuthManager, type UserContext } from '@tm/core/auth'; import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/** /**
* Result type from context command * Result type from context command
@@ -116,7 +119,8 @@ export class ContextCommand extends Command {
const result = this.displayContext(); const result = this.displayContext();
this.setLastResult(result); this.setLastResult(result);
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -212,7 +216,8 @@ export class ContextCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -253,7 +258,6 @@ export class ContextCommand extends Command {
this.authManager.updateContext({ this.authManager.updateContext({
orgId: selectedOrg.id, orgId: selectedOrg.id,
orgName: selectedOrg.name, orgName: selectedOrg.name,
orgSlug: selectedOrg.slug,
// Clear brief when changing org // Clear brief when changing org
briefId: undefined, briefId: undefined,
briefName: undefined briefName: undefined
@@ -300,7 +304,8 @@ export class ContextCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -424,7 +429,8 @@ export class ContextCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -470,7 +476,8 @@ export class ContextCommand extends Command {
process.exit(1); process.exit(1);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -506,13 +513,11 @@ export class ContextCommand extends Command {
process.exit(1); process.exit(1);
} }
// Fetch org to get a friendly name and slug (optional) // Fetch org to get a friendly name (optional)
let orgName: string | undefined; let orgName: string | undefined;
let orgSlug: string | undefined;
try { try {
const org = await this.authManager.getOrganization(brief.accountId); const org = await this.authManager.getOrganization(brief.accountId);
orgName = org?.name; orgName = org?.name;
orgSlug = org?.slug;
} catch { } catch {
// Non-fatal if org lookup fails // Non-fatal if org lookup fails
} }
@@ -523,7 +528,6 @@ export class ContextCommand extends Command {
this.authManager.updateContext({ this.authManager.updateContext({
orgId: brief.accountId, orgId: brief.accountId,
orgName, orgName,
orgSlug,
briefId: brief.id, briefId: brief.id,
briefName briefName
}); });
@@ -545,7 +549,8 @@ export class ContextCommand extends Command {
try { try {
if (spinner?.isSpinning) spinner.stop(); if (spinner?.isSpinning) spinner.stop();
} catch {} } catch {}
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -674,6 +679,26 @@ export class ContextCommand extends Command {
} }
} }
/**
* Handle errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
console.error(chalk.red(`\n✗ ${error.message}`));
if (error.code === 'NOT_AUTHENTICATED') {
ui.displayWarning('Please authenticate first: tm auth login');
}
} else {
const msg = error?.message ?? String(error);
console.error(chalk.red(`Error: ${msg}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/** /**
* Set the last result for programmatic access * Set the last result for programmatic access
*/ */

View File

@@ -7,10 +7,13 @@ import { Command } from 'commander';
import chalk from 'chalk'; import chalk from 'chalk';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import ora, { Ora } from 'ora'; import ora, { Ora } from 'ora';
import { AuthManager, type UserContext } from '@tm/core/auth'; import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import { TaskMasterCore, type ExportResult } from '@tm/core'; import { TaskMasterCore, type ExportResult } from '@tm/core';
import * as ui from '../utils/ui.js'; import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/** /**
* Result type from export command * Result type from export command
@@ -194,7 +197,8 @@ export class ExportCommand extends Command {
}; };
} catch (error: any) { } catch (error: any) {
if (spinner?.isSpinning) spinner.fail('Export failed'); if (spinner?.isSpinning) spinner.fail('Export failed');
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -330,6 +334,26 @@ export class ExportCommand extends Command {
return confirmed; return confirmed;
} }
/**
* Handle errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
console.error(chalk.red(`\n✗ ${error.message}`));
if (error.code === 'NOT_AUTHENTICATED') {
ui.displayWarning('Please authenticate first: tm auth login');
}
} else {
const msg = error?.message ?? String(error);
console.error(chalk.red(`Error: ${msg}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/** /**
* Get the last export result (useful for testing) * Get the last export result (useful for testing)
*/ */

View File

@@ -17,9 +17,8 @@ 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 { displayCommandHeader } from '../utils/display-helpers.js';
import { import {
displayHeader,
displayDashboards, displayDashboards,
calculateTaskStatistics, calculateTaskStatistics,
calculateSubtaskStatistics, calculateSubtaskStatistics,
@@ -107,7 +106,14 @@ export class ListTasksCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); 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);
} }
} }
@@ -251,12 +257,15 @@ export class ListTasksCommand extends Command {
* Display in text format with tables * Display in text format with tables
*/ */
private displayText(data: ListTasksResult, withSubtasks?: boolean): void { private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
const { tasks, tag, storageType } = data; const { tasks, tag } = data;
// Display header using utility function // Get file path for display
displayCommandHeader(this.tmCore, { const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
// Display header without banner (banner already shown by main CLI)
displayHeader({
tag: tag || 'master', tag: tag || 'master',
storageType filePath: filePath
}); });
// No tasks message // No tasks message

View File

@@ -9,9 +9,8 @@ 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 { displayCommandHeader } from '../utils/display-helpers.js'; import { displayHeader } from '../ui/index.js';
/** /**
* Options interface for the next command * Options interface for the next command
@@ -59,7 +58,6 @@ export class NextCommand extends Command {
* Execute the next command * Execute the next command
*/ */
private async executeCommand(options: NextCommandOptions): Promise<void> { private async executeCommand(options: NextCommandOptions): Promise<void> {
let hasError = false;
try { try {
// Validate options (throws on invalid options) // Validate options (throws on invalid options)
this.validateOptions(options); this.validateOptions(options);
@@ -78,17 +76,16 @@ export class NextCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
hasError = true; const msg = error?.getSanitizedDetails?.() ?? {
displayError(error, { skipExit: true }); 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();
} }
// Exit after cleanup completes
if (hasError) {
process.exit(1);
}
} }
/** /**
@@ -173,10 +170,9 @@ export class NextCommand extends Command {
* Display in text format * Display in text format
*/ */
private displayText(result: NextTaskResult): void { private displayText(result: NextTaskResult): void {
// Display header with storage info // Display header with tag (no file path for next command)
displayCommandHeader(this.tmCore, { displayHeader({
tag: result.tag || 'master', tag: result.tag || 'master'
storageType: result.storageType
}); });
if (!result.found || !result.task) { if (!result.found || !result.task) {
@@ -195,6 +191,7 @@ export class NextCommand extends Command {
} }
) )
); );
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
console.log( console.log(
`\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}` `\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}`
); );
@@ -211,6 +208,8 @@ export class NextCommand extends Command {
headerColor: 'green', headerColor: 'green',
showSuggestedActions: true showSuggestedActions: true
}); });
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
} }
/** /**

View File

@@ -12,7 +12,6 @@ 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
@@ -86,7 +85,6 @@ export class SetStatusCommand extends Command {
private async executeCommand( private async executeCommand(
options: SetStatusCommandOptions options: SetStatusCommandOptions
): Promise<void> { ): Promise<void> {
let hasError = false;
try { try {
// Validate required options // Validate required options
if (!options.id) { if (!options.id) {
@@ -137,15 +135,16 @@ export class SetStatusCommand extends Command {
oldStatus: result.oldStatus, oldStatus: result.oldStatus,
newStatus: result.newStatus newStatus: result.newStatus
}); });
} catch (error: any) { } catch (error) {
hasError = true; const errorMessage =
if (options.format === 'json') { error instanceof Error ? error.message : String(error);
const errorMessage = error?.getSanitizedDetails
? error.getSanitizedDetails().message
: error instanceof Error
? error.message
: String(error);
if (!options.silent) {
console.error(
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
);
}
if (options.format === 'json') {
console.log( console.log(
JSON.stringify({ JSON.stringify({
success: false, success: false,
@@ -154,13 +153,8 @@ export class SetStatusCommand extends Command {
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}) })
); );
} else if (!options.silent) {
// Show which task failed with context
console.error(chalk.red(`\nFailed to update task ${taskId}:`));
displayError(error, { skipExit: true });
} }
// Don't exit here - let finally block clean up first process.exit(1);
break;
} }
} }
@@ -176,26 +170,25 @@ export class SetStatusCommand extends Command {
// Display results // Display results
this.displayResults(this.lastResult, options); this.displayResults(this.lastResult, options);
} catch (error: any) { } catch (error) {
hasError = true; const errorMessage =
if (options.format === 'json') { error instanceof Error ? error.message : 'Unknown error occurred';
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred'; if (!options.silent) {
console.log(JSON.stringify({ success: false, error: errorMessage })); console.error(chalk.red(`Error: ${errorMessage}`));
} else if (!options.silent) {
displayError(error, { skipExit: true });
} }
if (options.format === 'json') {
console.log(JSON.stringify({ success: false, error: errorMessage }));
}
process.exit(1);
} finally { } finally {
// Clean up resources // Clean up resources
if (this.tmCore) { if (this.tmCore) {
await this.tmCore.close(); await this.tmCore.close();
} }
} }
// Exit after cleanup completes
if (hasError) {
process.exit(1);
}
} }
/** /**

View File

@@ -9,9 +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';
import { displayCommandHeader } from '../utils/display-helpers.js';
/** /**
* Options interface for the show command * Options interface for the show command
@@ -114,7 +112,14 @@ export class ShowCommand extends Command {
this.displayResults(result, options); this.displayResults(result, options);
} }
} catch (error: any) { } catch (error: any) {
displayError(error); 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);
} }
} }
@@ -252,15 +257,6 @@ export class ShowCommand extends Command {
return; return;
} }
// Display header with storage info
const activeTag = this.tmCore?.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
console.log(); // Add spacing
// Use the global task details display function // Use the global task details display function
displayTaskDetails(result.task, { displayTaskDetails(result.task, {
statusFilter: options.status, statusFilter: options.status,
@@ -275,12 +271,8 @@ export class ShowCommand extends Command {
result: ShowMultipleTasksResult, result: ShowMultipleTasksResult,
_options: ShowCommandOptions _options: ShowCommandOptions
): void { ): void {
// Display header with storage info // Header
const activeTag = this.tmCore?.getActiveTag() || 'master'; ui.displayBanner(`Tasks (${result.tasks.length} found)`);
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
if (result.notFound.length > 0) { if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`)); console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
@@ -299,6 +291,8 @@ export class ShowCommand extends Command {
showDependencies: true showDependencies: true
}) })
); );
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
} }
/** /**

View File

@@ -16,7 +16,6 @@ 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
@@ -161,7 +160,8 @@ export class StartCommand extends Command {
if (spinner) { if (spinner) {
spinner.fail('Operation failed'); spinner.fail('Operation failed');
} }
displayError(error); this.handleError(error);
process.exit(1);
} }
} }
@@ -452,6 +452,22 @@ 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,9 +24,6 @@ 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

@@ -5,16 +5,6 @@
import chalk from 'chalk'; import chalk from 'chalk';
/**
* Brief information for API storage
*/
export interface BriefInfo {
briefId: string;
briefName: string;
orgSlug?: string;
webAppUrl?: string;
}
/** /**
* Header configuration options * Header configuration options
*/ */
@@ -22,44 +12,16 @@ export interface HeaderOptions {
title?: string; title?: string;
tag?: string; tag?: string;
filePath?: string; filePath?: string;
storageType?: 'api' | 'file';
briefInfo?: BriefInfo;
} }
/** /**
* Display the Task Master header with project info * Display the Task Master header with project info
*/ */
export function displayHeader(options: HeaderOptions = {}): void { export function displayHeader(options: HeaderOptions = {}): void {
const { filePath, tag, storageType, briefInfo } = options; const { filePath, tag } = options;
// Display different header based on storage type // Display tag and file path info
if (storageType === 'api' && briefInfo) { if (tag) {
// API storage: Show brief information
const briefDisplay = `🏷 Brief: ${chalk.cyan(briefInfo.briefName)} ${chalk.gray(`(${briefInfo.briefId})`)}`;
console.log(briefDisplay);
// Construct and display the brief URL or ID
if (briefInfo.webAppUrl && briefInfo.orgSlug) {
const briefUrl = `${briefInfo.webAppUrl}/home/${briefInfo.orgSlug}/briefs/${briefInfo.briefId}/plan`;
console.log(`Listing tasks from: ${chalk.dim(briefUrl)}`);
} else if (briefInfo.webAppUrl) {
// Show web app URL and brief ID if org slug is missing
console.log(
`Listing tasks from: ${chalk.dim(`${briefInfo.webAppUrl} (Brief: ${briefInfo.briefId})`)}`
);
console.log(
chalk.yellow(
`💡 Tip: Run ${chalk.cyan('tm context select')} to set your organization and see the full URL`
)
);
} else {
// Fallback: just show the brief ID if we can't get web app URL
console.log(
`Listing tasks from: ${chalk.dim(`API (Brief ID: ${briefInfo.briefId})`)}`
);
}
} else if (tag) {
// File storage: Show tag information
let tagInfo = ''; let tagInfo = '';
if (tag && tag !== 'master') { if (tag && tag !== 'master') {

View File

@@ -1,75 +0,0 @@
/**
* @fileoverview Display helper utilities for commands
* Provides DRY utilities for displaying headers and other command output
*/
import type { TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types';
import { displayHeader, type BriefInfo } from '../ui/index.js';
/**
* Get web app base URL from environment
*/
function getWebAppUrl(): string | undefined {
const baseDomain =
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
if (!baseDomain) {
return undefined;
}
// If it already includes protocol, use as-is
if (baseDomain.startsWith('http://') || baseDomain.startsWith('https://')) {
return baseDomain;
}
// Otherwise, add protocol based on domain
if (baseDomain.includes('localhost') || baseDomain.includes('127.0.0.1')) {
return `http://${baseDomain}`;
}
return `https://${baseDomain}`;
}
/**
* Display the command header with appropriate storage information
* Handles both API and file storage displays
*/
export function displayCommandHeader(
tmCore: TaskMasterCore | undefined,
options: {
tag?: string;
storageType: Exclude<StorageType, 'auto'>;
}
): void {
const { tag, storageType } = options;
// Get brief info if using API storage
let briefInfo: BriefInfo | undefined;
if (storageType === 'api' && tmCore) {
const storageInfo = tmCore.getStorageDisplayInfo();
if (storageInfo) {
// Construct full brief info with web app URL
briefInfo = {
...storageInfo,
webAppUrl: getWebAppUrl()
};
}
}
// 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`
: undefined;
// Display header
displayHeader({
tag: tag || 'master',
filePath: filePath,
storageType: storageType === 'api' ? 'api' : 'file',
briefInfo: briefInfo
});
}

View File

@@ -1,60 +0,0 @@
/**
* @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

@@ -5,6 +5,45 @@ sidebarTitle: "CLI Commands"
<AccordionGroup> <AccordionGroup>
<Accordion title="Authentication">
```bash
# Log in to tryhamster.com (opens browser for OAuth authentication)
task-master auth login
# Display current authentication status
task-master auth status
# Log out and clear stored credentials
task-master auth logout
# Refresh authentication token
task-master auth refresh
```
**Note**: After successful login, Task Master will automatically prompt you to configure your workspace context (organization and project selection). If context setup encounters issues, you can configure it later using `task-master context`.
</Accordion>
<Accordion title="Workspace Context">
```bash
# Show current workspace context (organization and brief)
task-master context
# Select an organization
task-master context org
# Select a brief within the current organization
task-master context brief
# Set context directly with IDs
task-master context set --org <orgId> --brief <briefId>
# Clear all context selections
task-master context clear
```
**Note**: Workspace context determines which organization and project brief your tasks and data are associated with. This is automatically configured during initial login but can be changed anytime.
</Accordion>
<Accordion title="Parse PRD"> <Accordion title="Parse PRD">
```bash ```bash
# Parse a PRD file and generate tasks # Parse a PRD file and generate tasks

44
output.txt Normal file

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,6 @@ export interface AuthCredentials {
export interface UserContext { export interface UserContext {
orgId?: string; orgId?: string;
orgName?: string; orgName?: string;
orgSlug?: string;
briefId?: string; briefId?: string;
briefName?: string; briefName?: string;
updatedAt: string; updatedAt: string;

View File

@@ -52,10 +52,7 @@ 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

@@ -25,7 +25,7 @@ export interface LoggerConfig {
export class Logger { export class Logger {
private config: Required<LoggerConfig>; private config: Required<LoggerConfig>;
private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = { private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = {
level: LogLevel.SILENT, level: LogLevel.WARN,
silent: false, silent: false,
prefix: '', prefix: '',
timestamp: false, timestamp: false,

View File

@@ -161,16 +161,6 @@ 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',
@@ -196,14 +186,6 @@ 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,
@@ -540,14 +522,6 @@ 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

@@ -37,13 +37,6 @@ export interface ApiStorageConfig {
maxRetries?: number; maxRetries?: number;
} }
/**
* Auth context with a guaranteed briefId
*/
type ContextWithBrief = NonNullable<
ReturnType<typeof AuthManager.prototype.getContext>
> & { briefId: string };
/** /**
* ApiStorage implementation using repository pattern * ApiStorage implementation using repository pattern
* Provides flexibility to swap between different backend implementations * Provides flexibility to swap between different backend implementations
@@ -133,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 = authManager.getContext(); const context = await 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) {
@@ -165,7 +158,15 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
const context = this.ensureBriefSelected('loadTasks'); const authManager = AuthManager.getInstance();
const context = await 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>'
);
}
// Load tasks from the current brief context with filters pushed to repository // Load tasks from the current brief context with filters pushed to repository
const tasks = await this.retryOperation(() => const tasks = await this.retryOperation(() =>
@@ -180,11 +181,12 @@ export class ApiStorage implements IStorage {
return tasks; return tasks;
} catch (error) { } catch (error) {
this.wrapError(error, 'Failed to load tasks from API', { throw new TaskMasterError(
operation: 'loadTasks', 'Failed to load tasks from API',
tag, ERROR_CODES.STORAGE_ERROR,
context: 'brief-based loading' { operation: 'loadTasks', tag, context: 'brief-based loading' },
}); error as Error
);
} }
} }
@@ -235,17 +237,16 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
this.ensureBriefSelected('loadTask');
return await this.retryOperation(() => return await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId) this.repository.getTask(this.projectId, taskId)
); );
} catch (error) { } catch (error) {
this.wrapError(error, 'Failed to load task from API', { throw new TaskMasterError(
operation: 'loadTask', 'Failed to load task from API',
taskId, ERROR_CODES.STORAGE_ERROR,
tag { operation: 'loadTask', taskId, tag },
}); error as Error
);
} }
} }
@@ -324,7 +325,7 @@ export class ApiStorage implements IStorage {
try { try {
const authManager = AuthManager.getInstance(); const authManager = AuthManager.getInstance();
const context = authManager.getContext(); const context = await 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) {
@@ -509,8 +510,6 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized(); await this.ensureInitialized();
try { try {
this.ensureBriefSelected('updateTaskStatus');
const existingTask = await this.retryOperation(() => const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId) this.repository.getTask(this.projectId, taskId)
); );
@@ -547,12 +546,12 @@ export class ApiStorage implements IStorage {
taskId taskId
}; };
} catch (error) { } catch (error) {
this.wrapError(error, 'Failed to update task status via API', { throw new TaskMasterError(
operation: 'updateTaskStatus', 'Failed to update task status via API',
taskId, ERROR_CODES.STORAGE_ERROR,
newStatus, { operation: 'updateTaskStatus', taskId, newStatus, tag },
tag error as Error
}); );
} }
} }
@@ -770,29 +769,6 @@ 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): ContextWithBrief {
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 <brief-id> or tm context brief <brief-url>'
}
);
}
return context as ContextWithBrief;
}
/** /**
* Retry an operation with exponential backoff * Retry an operation with exponential backoff
*/ */
@@ -811,28 +787,4 @@ export class ApiStorage implements IStorage {
throw error; throw error;
} }
} }
/**
* Wrap an error unless it's already a NO_BRIEF_SELECTED error
*/
private wrapError(
error: unknown,
message: string,
context: Record<string, unknown>
): 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
);
}
} }

View File

@@ -201,44 +201,6 @@ export class TaskMasterCore {
return this.taskService.getStorageType(); return this.taskService.getStorageType();
} }
/**
* Get storage configuration
*/
getStorageConfig() {
return this.configManager.getStorageConfig();
}
/**
* Get storage display information for headers
* Returns context info for API storage, null for file storage
*/
getStorageDisplayInfo(): {
briefId: string;
briefName: string;
orgSlug?: string;
} | null {
// Only return info if using API storage
const storageType = this.getStorageType();
if (storageType !== 'api') {
return null;
}
// Get credentials from auth manager
const authManager = AuthManager.getInstance();
const credentials = authManager.getCredentials();
const selectedContext = credentials?.selectedContext;
if (!selectedContext?.briefId || !selectedContext?.briefName) {
return null;
}
return {
briefId: selectedContext.briefId,
briefName: selectedContext.briefName,
orgSlug: selectedContext.orgSlug
};
}
/** /**
* Get current active tag * Get current active tag
*/ */

View File

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