Compare commits
2 Commits
docs/auto-
...
ralph/fix/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02b2120935 | ||
|
|
662e3865f3 |
@@ -15,6 +15,7 @@ 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
|
||||||
@@ -117,8 +118,7 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +134,7 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +146,7 @@ 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) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +162,7 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +387,7 @@ export class AuthCommand extends Command {
|
|||||||
message: 'Authentication successful'
|
message: 'Authentication successful'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleAuthError(error as AuthenticationError);
|
displayError(error, { skipExit: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -453,51 +450,6 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,12 +8,9 @@ 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 {
|
import { AuthManager, type UserContext } from '@tm/core/auth';
|
||||||
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
|
||||||
@@ -119,8 +116,7 @@ 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) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +212,7 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +253,7 @@ 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
|
||||||
@@ -304,8 +300,7 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,8 +424,7 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,8 +470,7 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,11 +506,13 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch org to get a friendly name (optional)
|
// Fetch org to get a friendly name and slug (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
|
||||||
}
|
}
|
||||||
@@ -528,6 +523,7 @@ 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
|
||||||
});
|
});
|
||||||
@@ -549,8 +545,7 @@ export class ContextCommand extends Command {
|
|||||||
try {
|
try {
|
||||||
if (spinner?.isSpinning) spinner.stop();
|
if (spinner?.isSpinning) spinner.stop();
|
||||||
} catch {}
|
} catch {}
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,26 +674,6 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ 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 {
|
import { AuthManager, type UserContext } from '@tm/core/auth';
|
||||||
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
|
||||||
@@ -197,8 +194,7 @@ 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');
|
||||||
this.handleError(error);
|
displayError(error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,26 +330,6 @@ 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)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ 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,
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,15 +251,12 @@ 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 } = data;
|
const { tasks, tag, storageType } = data;
|
||||||
|
|
||||||
// Get file path for display
|
// Display header using utility function
|
||||||
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
|
displayCommandHeader(this.tmCore, {
|
||||||
|
|
||||||
// Display header without banner (banner already shown by main CLI)
|
|
||||||
displayHeader({
|
|
||||||
tag: tag || 'master',
|
tag: tag || 'master',
|
||||||
filePath: filePath
|
storageType
|
||||||
});
|
});
|
||||||
|
|
||||||
// No tasks message
|
// No tasks message
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ 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 { displayCommandHeader } from '../utils/display-helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options interface for the next command
|
* Options interface for the next command
|
||||||
@@ -58,6 +59,7 @@ 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);
|
||||||
@@ -76,16 +78,17 @@ export class NextCommand extends Command {
|
|||||||
this.displayResults(result, options);
|
this.displayResults(result, options);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = error?.getSanitizedDetails?.() ?? {
|
hasError = true;
|
||||||
message: error?.message ?? String(error)
|
displayError(error, { skipExit: true });
|
||||||
};
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,9 +173,10 @@ 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 tag (no file path for next command)
|
// Display header with storage info
|
||||||
displayHeader({
|
displayCommandHeader(this.tmCore, {
|
||||||
tag: result.tag || 'master'
|
tag: result.tag || 'master',
|
||||||
|
storageType: result.storageType
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.found || !result.task) {
|
if (!result.found || !result.task) {
|
||||||
@@ -191,7 +195,6 @@ 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')}`
|
||||||
);
|
);
|
||||||
@@ -208,8 +211,6 @@ export class NextCommand extends Command {
|
|||||||
headerColor: 'green',
|
headerColor: 'green',
|
||||||
showSuggestedActions: true
|
showSuggestedActions: true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -85,6 +86,7 @@ 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) {
|
||||||
@@ -135,16 +137,15 @@ export class SetStatusCommand extends Command {
|
|||||||
oldStatus: result.oldStatus,
|
oldStatus: result.oldStatus,
|
||||||
newStatus: result.newStatus
|
newStatus: result.newStatus
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
const errorMessage =
|
hasError = true;
|
||||||
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 +154,13 @@ 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 });
|
||||||
}
|
}
|
||||||
process.exit(1);
|
// Don't exit here - let finally block clean up first
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,25 +176,26 @@ export class SetStatusCommand extends Command {
|
|||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
this.displayResults(this.lastResult, options);
|
this.displayResults(this.lastResult, options);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
const errorMessage =
|
hasError = true;
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
|
||||||
|
|
||||||
if (!options.silent) {
|
|
||||||
console.error(chalk.red(`Error: ${errorMessage}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.format === 'json') {
|
if (options.format === 'json') {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||||
|
} else if (!options.silent) {
|
||||||
|
displayError(error, { skipExit: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ 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
|
||||||
@@ -112,14 +114,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +252,15 @@ 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,
|
||||||
@@ -271,8 +275,12 @@ export class ShowCommand extends Command {
|
|||||||
result: ShowMultipleTasksResult,
|
result: ShowMultipleTasksResult,
|
||||||
_options: ShowCommandOptions
|
_options: ShowCommandOptions
|
||||||
): void {
|
): void {
|
||||||
// Header
|
// Display header with storage info
|
||||||
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
|
const activeTag = this.tmCore?.getActiveTag() || 'master';
|
||||||
|
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(', ')}`));
|
||||||
@@ -291,8 +299,6 @@ export class ShowCommand extends Command {
|
|||||||
showDependencies: true
|
showDependencies: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
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
|
||||||
*/
|
*/
|
||||||
@@ -12,16 +22,44 @@ 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 } = options;
|
const { filePath, tag, storageType, briefInfo } = options;
|
||||||
|
|
||||||
// Display tag and file path info
|
// Display different header based on storage type
|
||||||
if (tag) {
|
if (storageType === 'api' && briefInfo) {
|
||||||
|
// 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') {
|
||||||
|
|||||||
75
apps/cli/src/utils/display-helpers.ts
Normal file
75
apps/cli/src/utils/display-helpers.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
|
});
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ 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;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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.WARN,
|
level: LogLevel.SILENT,
|
||||||
silent: false,
|
silent: false,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
timestamp: false,
|
timestamp: false,
|
||||||
|
|||||||
@@ -161,6 +161,16 @@ 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 +196,14 @@ 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 +540,14 @@ 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,
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ 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
|
||||||
@@ -126,7 +133,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) {
|
||||||
@@ -158,15 +165,7 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const context = this.ensureBriefSelected('loadTasks');
|
||||||
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(() =>
|
||||||
@@ -181,12 +180,11 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
return tasks;
|
return tasks;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
this.wrapError(error, 'Failed to load tasks from API', {
|
||||||
'Failed to load tasks from API',
|
operation: 'loadTasks',
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
tag,
|
||||||
{ operation: 'loadTasks', tag, context: 'brief-based loading' },
|
context: 'brief-based loading'
|
||||||
error as Error
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,16 +235,17 @@ 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) {
|
||||||
throw new TaskMasterError(
|
this.wrapError(error, 'Failed to load task from API', {
|
||||||
'Failed to load task from API',
|
operation: 'loadTask',
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
taskId,
|
||||||
{ operation: 'loadTask', taskId, tag },
|
tag
|
||||||
error as Error
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +324,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 +509,8 @@ 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)
|
||||||
);
|
);
|
||||||
@@ -546,12 +547,12 @@ export class ApiStorage implements IStorage {
|
|||||||
taskId
|
taskId
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
this.wrapError(error, 'Failed to update task status via API', {
|
||||||
'Failed to update task status via API',
|
operation: 'updateTaskStatus',
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
taskId,
|
||||||
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
|
newStatus,
|
||||||
error as Error
|
tag
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,6 +770,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): 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
|
||||||
*/
|
*/
|
||||||
@@ -787,4 +811,28 @@ 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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,44 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user