Compare commits

..

9 Commits

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

  Original commit: feat: handle new command errors better (#1318)\n\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
2025-10-16 20:39:02 +00:00
Ralph Khreish
662e3865f3 feat: handle new command errors better (#1318) 2025-10-16 22:31:50 +02:00
Ralph Khreish
8649c8a347 chore: apply requested coderabbit changes 2025-10-16 19:24:29 +02:00
Ralph Khreish
f7cab246b0 fix: storage issue (show file or api more consistently) 2025-10-16 19:24:29 +02:00
Ralph Khreish
5aca107827 fix: runtime env variables working with new tm_ env variables 2025-10-16 19:24:29 +02:00
Ralph Khreish
fb68c9fe1f feat: improve auth login by adding context selection immediately after logging in 2025-10-16 19:24:29 +02:00
Ralph Khreish
ff3bd7add8 chore: CI format 2025-10-16 19:24:29 +02:00
Ralph Khreish
c8228e913b feat: show brief title when listing brief instead of uuid
- add search for brief selection
2025-10-16 19:24:29 +02:00
Ralph Khreish
218b68a31e feat: implement runtime and build time env variables for remote access 2025-10-16 19:24:29 +02:00
33 changed files with 984 additions and 331 deletions

View File

@@ -22,6 +22,7 @@
"test:ci": "vitest run --coverage --reporter=dot"
},
"dependencies": {
"@inquirer/search": "^3.2.0",
"@tm/core": "*",
"boxen": "^8.0.1",
"chalk": "5.6.2",

View File

@@ -14,6 +14,8 @@ import {
type AuthCredentials
} from '@tm/core/auth';
import * as ui from '../utils/ui.js';
import { ContextCommand } from './context.command.js';
import { displayError } from '../utils/error-handler.js';
/**
* Result type from auth command
@@ -116,8 +118,7 @@ export class AuthCommand extends Command {
process.exit(0);
}, 100);
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -133,8 +134,7 @@ export class AuthCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -146,8 +146,7 @@ export class AuthCommand extends Command {
const result = this.displayStatus();
this.setLastResult(result);
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -163,8 +162,7 @@ export class AuthCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -351,6 +349,37 @@ export class AuthCommand extends Command {
chalk.gray(` Logged in as: ${credentials.email || credentials.userId}`)
);
// Post-auth: Set up workspace context
console.log(); // Add spacing
try {
const contextCommand = new ContextCommand();
const contextResult = await contextCommand.setupContextInteractive();
if (contextResult.success) {
if (contextResult.orgSelected && contextResult.briefSelected) {
console.log(
chalk.green('✓ Workspace context configured successfully')
);
} else if (contextResult.orgSelected) {
console.log(chalk.green('✓ Organization selected'));
}
} else {
console.log(
chalk.yellow('⚠ Context setup was skipped or encountered issues')
);
console.log(
chalk.gray(' You can set up context later with "tm context"')
);
}
} catch (contextError) {
console.log(chalk.yellow('⚠ Context setup encountered an error'));
console.log(
chalk.gray(' You can set up context later with "tm context"')
);
if (process.env.DEBUG) {
console.error(chalk.gray((contextError as Error).message));
}
}
return {
success: true,
action: 'login',
@@ -358,7 +387,7 @@ export class AuthCommand extends Command {
message: 'Authentication successful'
};
} catch (error) {
this.handleAuthError(error as AuthenticationError);
displayError(error, { skipExit: true });
return {
success: false,
@@ -421,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
*/

View File

@@ -6,13 +6,11 @@
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora, { Ora } from 'ora';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import { AuthManager, type UserContext } from '@tm/core/auth';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/**
* Result type from context command
@@ -118,8 +116,7 @@ export class ContextCommand extends Command {
const result = this.displayContext();
this.setLastResult(result);
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -156,10 +153,14 @@ export class ContextCommand extends Command {
if (context.briefName || context.briefId) {
console.log(chalk.green('\n✓ Brief'));
if (context.briefName) {
if (context.briefName && context.briefId) {
const shortId = context.briefId.slice(0, 8);
console.log(
chalk.white(` ${context.briefName} `) + chalk.gray(`(${shortId})`)
);
} else if (context.briefName) {
console.log(chalk.white(` ${context.briefName}`));
}
if (context.briefId) {
} else if (context.briefId) {
console.log(chalk.gray(` ID: ${context.briefId}`));
}
}
@@ -211,8 +212,7 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -253,6 +253,7 @@ export class ContextCommand extends Command {
this.authManager.updateContext({
orgId: selectedOrg.id,
orgName: selectedOrg.name,
orgSlug: selectedOrg.slug,
// Clear brief when changing org
briefId: undefined,
briefName: undefined
@@ -299,8 +300,7 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -324,25 +324,53 @@ export class ContextCommand extends Command {
};
}
// Prompt for selection
const { selectedBrief } = await inquirer.prompt([
{
type: 'list',
name: 'selectedBrief',
message: 'Select a brief:',
choices: [
{ name: '(No brief - organization level)', value: null },
...briefs.map((brief) => ({
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
value: brief
}))
]
// Prompt for selection with search
const selectedBrief = await search<(typeof briefs)[0] | null>({
message: 'Search for a brief:',
source: async (input) => {
const searchTerm = input?.toLowerCase() || '';
// Static option for no brief
const noBriefOption = {
name: '(No brief - organization level)',
value: null as any,
description: 'Clear brief selection'
};
// Filter and map brief options
const briefOptions = briefs
.filter((brief) => {
if (!searchTerm) return true;
const title = brief.document?.title || '';
const shortId = brief.id.slice(0, 8);
// Search by title first, then by UUID
return (
title.toLowerCase().includes(searchTerm) ||
brief.id.toLowerCase().includes(searchTerm) ||
shortId.toLowerCase().includes(searchTerm)
);
})
.map((brief) => {
const title =
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
const shortId = brief.id.slice(0, 8);
return {
name: `${title} ${chalk.gray(`(${shortId})`)}`,
value: brief
};
});
return [noBriefOption, ...briefOptions];
}
]);
});
if (selectedBrief) {
// Update context with brief
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
const briefName =
selectedBrief.document?.title ||
`Brief ${selectedBrief.id.slice(0, 8)}`;
this.authManager.updateContext({
briefId: selectedBrief.id,
briefName: briefName
@@ -354,7 +382,7 @@ export class ContextCommand extends Command {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
message: `Selected brief: ${selectedBrief.name}`
message: `Selected brief: ${selectedBrief.document?.title}`
};
} else {
// Clear brief selection
@@ -396,8 +424,7 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -443,8 +470,7 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -468,7 +494,7 @@ export class ContextCommand extends Command {
if (!briefId) {
spinner.fail('Could not extract a brief ID from the provided input');
ui.displayError(
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
);
process.exit(1);
}
@@ -480,20 +506,24 @@ export class ContextCommand extends Command {
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 orgSlug: string | undefined;
try {
const org = await this.authManager.getOrganization(brief.accountId);
orgName = org?.name;
orgSlug = org?.slug;
} catch {
// Non-fatal if org lookup fails
}
// Update context: set org and brief
const briefName = `Brief ${brief.id.slice(0, 8)}`;
const briefName =
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
this.authManager.updateContext({
orgId: brief.accountId,
orgName,
orgSlug,
briefId: brief.id,
briefName
});
@@ -515,8 +545,7 @@ export class ContextCommand extends Command {
try {
if (spinner?.isSpinning) spinner.stop();
} catch {}
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -645,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
*/
@@ -686,6 +695,53 @@ export class ContextCommand extends Command {
return this.authManager.getContext();
}
/**
* Interactive context setup (for post-auth flow)
* Prompts user to select org and brief
*/
async setupContextInteractive(): Promise<{
success: boolean;
orgSelected: boolean;
briefSelected: boolean;
}> {
try {
// Ask if user wants to set up workspace context
const { setupContext } = await inquirer.prompt([
{
type: 'confirm',
name: 'setupContext',
message: 'Would you like to set up your workspace context now?',
default: true
}
]);
if (!setupContext) {
return { success: true, orgSelected: false, briefSelected: false };
}
// Select organization
const orgResult = await this.selectOrganization();
if (!orgResult.success || !orgResult.context?.orgId) {
return { success: false, orgSelected: false, briefSelected: false };
}
// Select brief
const briefResult = await this.selectBrief(orgResult.context.orgId);
return {
success: true,
orgSelected: true,
briefSelected: briefResult.success
};
} catch (error) {
console.error(
chalk.yellow(
'\nContext setup skipped due to error. You can set it up later with "tm context"'
)
);
return { success: false, orgSelected: false, briefSelected: false };
}
}
/**
* Clean up resources
*/

View File

@@ -7,13 +7,10 @@ import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora, { Ora } from 'ora';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import { AuthManager, type UserContext } from '@tm/core/auth';
import { TaskMasterCore, type ExportResult } from '@tm/core';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/**
* Result type from export command
@@ -197,8 +194,7 @@ export class ExportCommand extends Command {
};
} catch (error: any) {
if (spinner?.isSpinning) spinner.fail('Export failed');
this.handleError(error);
process.exit(1);
displayError(error);
}
}
@@ -334,26 +330,6 @@ export class ExportCommand extends Command {
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)
*/

View File

@@ -17,8 +17,9 @@ import {
} from '@tm/core';
import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
import { displayCommandHeader } from '../utils/display-helpers.js';
import {
displayHeader,
displayDashboards,
calculateTaskStatistics,
calculateSubtaskStatistics,
@@ -106,14 +107,7 @@ export class ListTasksCommand extends Command {
this.displayResults(result, options);
}
} catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
displayError(error);
}
}
@@ -257,15 +251,12 @@ export class ListTasksCommand extends Command {
* Display in text format with tables
*/
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
const { tasks, tag } = data;
const { tasks, tag, storageType } = data;
// Get file path for display
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
// Display header without banner (banner already shown by main CLI)
displayHeader({
// Display header using utility function
displayCommandHeader(this.tmCore, {
tag: tag || 'master',
filePath: filePath
storageType
});
// No tasks message

View File

@@ -9,8 +9,9 @@ import chalk from 'chalk';
import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types';
import { displayError } from '../utils/error-handler.js';
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayHeader } from '../ui/index.js';
import { displayCommandHeader } from '../utils/display-helpers.js';
/**
* Options interface for the next command
@@ -58,6 +59,7 @@ export class NextCommand extends Command {
* Execute the next command
*/
private async executeCommand(options: NextCommandOptions): Promise<void> {
let hasError = false;
try {
// Validate options (throws on invalid options)
this.validateOptions(options);
@@ -76,16 +78,17 @@ export class NextCommand extends Command {
this.displayResults(result, options);
}
} catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
// Allow error to propagate for library compatibility
throw new Error(msg.message || 'Unexpected error in next command');
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);
}
}
/**
@@ -170,9 +173,10 @@ export class NextCommand extends Command {
* Display in text format
*/
private displayText(result: NextTaskResult): void {
// Display header with tag (no file path for next command)
displayHeader({
tag: result.tag || 'master'
// Display header with storage info
displayCommandHeader(this.tmCore, {
tag: result.tag || 'master',
storageType: result.storageType
});
if (!result.found || !result.task) {
@@ -191,7 +195,6 @@ export class NextCommand extends Command {
}
)
);
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
console.log(
`\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',
showSuggestedActions: true
});
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**

View File

@@ -12,6 +12,7 @@ import {
type TaskStatus
} from '@tm/core';
import type { StorageType } from '@tm/core/types';
import { displayError } from '../utils/error-handler.js';
/**
* Valid task status values for validation
@@ -85,6 +86,7 @@ export class SetStatusCommand extends Command {
private async executeCommand(
options: SetStatusCommandOptions
): Promise<void> {
let hasError = false;
try {
// Validate required options
if (!options.id) {
@@ -135,16 +137,15 @@ export class SetStatusCommand extends Command {
oldStatus: result.oldStatus,
newStatus: result.newStatus
});
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (!options.silent) {
console.error(
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
);
}
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage = error?.getSanitizedDetails
? error.getSanitizedDetails().message
: error instanceof Error
? error.message
: String(error);
console.log(
JSON.stringify({
success: false,
@@ -153,8 +154,13 @@ export class SetStatusCommand extends Command {
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
this.displayResults(this.lastResult, options);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
if (!options.silent) {
console.error(chalk.red(`Error: ${errorMessage}`));
}
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
console.log(JSON.stringify({ success: false, error: errorMessage }));
} else if (!options.silent) {
displayError(error, { skipExit: true });
}
process.exit(1);
} finally {
// Clean up resources
if (this.tmCore) {
await this.tmCore.close();
}
}
// Exit after cleanup completes
if (hasError) {
process.exit(1);
}
}
/**

View File

@@ -9,7 +9,9 @@ import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayCommandHeader } from '../utils/display-helpers.js';
/**
* Options interface for the show command
@@ -112,14 +114,7 @@ export class ShowCommand extends Command {
this.displayResults(result, options);
}
} catch (error: any) {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
displayError(error);
}
}
@@ -257,6 +252,15 @@ export class ShowCommand extends Command {
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
displayTaskDetails(result.task, {
statusFilter: options.status,
@@ -271,8 +275,12 @@ export class ShowCommand extends Command {
result: ShowMultipleTasksResult,
_options: ShowCommandOptions
): void {
// Header
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
// Display header with storage info
const activeTag = this.tmCore?.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
@@ -291,8 +299,6 @@ export class ShowCommand extends Command {
showDependencies: true
})
);
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**

View File

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

View File

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

View File

@@ -5,6 +5,16 @@
import chalk from 'chalk';
/**
* Brief information for API storage
*/
export interface BriefInfo {
briefId: string;
briefName: string;
orgSlug?: string;
webAppUrl?: string;
}
/**
* Header configuration options
*/
@@ -12,16 +22,44 @@ export interface HeaderOptions {
title?: string;
tag?: string;
filePath?: string;
storageType?: 'api' | 'file';
briefInfo?: BriefInfo;
}
/**
* Display the Task Master header with project info
*/
export function displayHeader(options: HeaderOptions = {}): void {
const { filePath, tag } = options;
const { filePath, tag, storageType, briefInfo } = options;
// Display tag and file path info
if (tag) {
// Display different header based on storage type
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 = '';
if (tag && tag !== 'master') {

View 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
});
}

View File

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

View File

@@ -3,6 +3,22 @@ title: CLI Commands
sidebarTitle: "CLI Commands"
---
## Debug Mode
For troubleshooting issues, you can enable debug mode to get detailed error information:
```bash
# Enable debug mode for any command
DEBUG=true task-master <command>
# Example with next command
DEBUG=true task-master next
```
Debug mode shows full error messages, stack traces, and additional context. See the [Troubleshooting guide](/getting-started/troubleshooting) for more details.
## Commands
<AccordionGroup>
<Accordion title="Parse PRD">
@@ -200,22 +216,6 @@ sidebarTitle: "CLI Commands"
```
</Accordion>
<Accordion title="Authentication">
```bash
# Login with browser authentication
task-master auth login
# Check authentication status
task-master auth status
# Refresh authentication token
task-master auth refresh
# Logout and clear credentials
task-master auth logout
```
</Accordion>
<Accordion title="Initialize a Project">
```bash
# Initialize a new project with Task Master structure

View File

@@ -33,6 +33,7 @@
]
},
"getting-started/api-keys",
"getting-started/troubleshooting",
"getting-started/faq",
"getting-started/contribute"
]

View File

@@ -3,7 +3,32 @@ title: FAQ
sidebarTitle: "FAQ"
---
Coming soon.
## Common Questions
### How do I get more detailed error information?
Enable debug mode by setting the `DEBUG` environment variable:
```bash
DEBUG=true task-master <command>
```
This shows full error messages, stack traces, and additional context. See our [Troubleshooting guide](/getting-started/troubleshooting) for complete details.
### Why are my commands failing silently?
Task Master uses sanitized error messages by default to provide a clean user experience. Enable debug mode to see technical details about what went wrong.
### How do I report a bug?
When reporting issues, please:
1. Enable debug mode and include the full error output
2. Provide your environment details (OS, Node.js version, Task Master version)
3. Include the command that failed and your project configuration
See our [Troubleshooting guide](/getting-started/troubleshooting) for complete bug reporting guidelines.
More frequently asked questions coming soon.
## 💬 Getting Help

View File

@@ -0,0 +1,155 @@
---
title: Troubleshooting
sidebarTitle: "Troubleshooting"
---
This guide helps you troubleshoot common issues and get more detailed error information when using Task Master.
## Debug Mode
Task Master includes a debug mode that provides detailed error information including stack traces. This is especially useful when reporting issues or debugging problems.
### Enabling Debug Mode
Set the `DEBUG` environment variable to enable debug mode:
```bash
# Enable debug mode for a single command
DEBUG=true task-master next
# Or use '1' instead of 'true'
DEBUG=1 task-master list
# Enable for entire session (bash/zsh)
export DEBUG=true
task-master next # Now shows detailed errors
# Enable for entire session (fish shell)
set -x DEBUG true
task-master next
```
### What Debug Mode Shows
With debug mode enabled, errors will include:
- **Full error messages**: Complete technical details instead of sanitized user messages
- **Stack traces**: Detailed information about where errors occurred
- **Error context**: Additional metadata about the failed operation
- **Error chains**: If an error was caused by another error, both are shown
### Example Output
**Normal mode (default):**
```
Error: Unable to read tasks file
```
**Debug mode:**
```
TaskMasterError[FILE_READ_ERROR]: Unable to read tasks file
Stack trace:
at TaskService.loadTasks (/path/to/task-service.ts:123)
at NextCommand.execute (/path/to/next.command.ts:45)
...
```
## Common Issues
### File Permission Errors
**Problem**: Commands fail with file access errors
**Solution**:
1. Check file permissions in `.taskmaster/` directory
2. Ensure your user has write access to the project directory
3. Run with debug mode to see the exact file path causing issues
### API Connection Issues
**Problem**: Commands timeout or fail when using API storage
**Solutions**:
1. Check your internet connection
2. Verify API keys are correctly configured
3. Check if your organization has firewall restrictions
4. Use debug mode to see specific network errors
### Task File Corruption
**Problem**: Commands report invalid task data
**Solutions**:
1. Check if `.taskmaster/tasks/tasks.json` is valid JSON
2. Restore from backup if available
3. Run `task-master generate` to regenerate task files from main data
### Model Configuration Issues
**Problem**: AI-powered commands fail
**Solutions**:
1. Verify API keys are set: `task-master models`
2. Check if the configured model is available
3. Try switching to a different model
4. Use debug mode to see specific API error messages
## Getting Help
When reporting issues, please:
1. **Enable debug mode** and include the full error output
2. **Specify your environment**:
- Operating system and version
- Node.js version (`node --version`)
- Task Master version (`task-master --version`)
3. **Include relevant context**:
- Command that failed
- Project configuration
- Recent changes to your setup
### Support Channels
- **GitHub Issues**: [Report bugs and request features](https://github.com/eyaltoledano/claude-task-master/issues)
- **Discord**: [Join our community for help](https://discord.gg/taskmasterai)
- **GitHub Discussions**: [Ask questions and share ideas](https://github.com/eyaltoledano/claude-task-master/discussions)
## Advanced Debugging
### Inspecting Task Data
If you suspect task data corruption:
```bash
# View raw task data
cat .taskmaster/tasks/tasks.json | jq '.'
# Check specific task
task-master show <task-id> --format=json
```
### Configuration Debugging
Check your current configuration:
```bash
# View model configuration
task-master models
# Check if initialization is complete
ls -la .taskmaster/
```
### Network Debugging
For API storage issues:
```bash
# Test with curl (replace with your API endpoint)
curl -H "Authorization: Bearer $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
https://api.anthropic.com/v1/models
```
Remember to enable debug mode (`DEBUG=true`) when troubleshooting to get the most helpful error information.

File diff suppressed because one or more lines are too long

86
package-lock.json generated
View File

@@ -104,6 +104,7 @@
"name": "@tm/cli",
"license": "MIT",
"dependencies": {
"@inquirer/search": "^3.2.0",
"@tm/core": "*",
"boxen": "^8.0.1",
"chalk": "5.6.2",
@@ -124,6 +125,91 @@
"node": ">=18.0.0"
}
},
"apps/cli/node_modules/@inquirer/ansi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz",
"integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"apps/cli/node_modules/@inquirer/figures": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz",
"integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"apps/cli/node_modules/@inquirer/search": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.0.tgz",
"integrity": "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ==",
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.0",
"@inquirer/figures": "^1.0.14",
"@inquirer/type": "^3.0.9",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"apps/cli/node_modules/@inquirer/search/node_modules/@inquirer/core": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz",
"integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==",
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.1",
"@inquirer/figures": "^1.0.14",
"@inquirer/type": "^3.0.9",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"apps/cli/node_modules/@inquirer/search/node_modules/@inquirer/type": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz",
"integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"apps/docs": {
"version": "0.0.6",
"devDependencies": {

View File

@@ -7,11 +7,13 @@ import path from 'path';
import { AuthConfig } from './types.js';
// Single base domain for all URLs
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsup's env option
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsdown's env option
// Runtime: process.env.TM_BASE_DOMAIN can override for staging/development
// Default: https://tryhamster.com for production
const BASE_DOMAIN =
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
'https://tryhamster.com';
process.env.TM_BASE_DOMAIN || // Runtime override (for staging/tux)
process.env.TM_PUBLIC_BASE_DOMAIN; // Build-time (baked into compiled code)
/**
* Default authentication configuration
@@ -19,7 +21,7 @@ const BASE_DOMAIN =
*/
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
// Base domain for all services
baseUrl: BASE_DOMAIN,
baseUrl: BASE_DOMAIN!,
// Configuration directory and file paths
configDir: path.join(os.homedir(), '.taskmaster'),

View File

@@ -24,6 +24,8 @@ export class CredentialStore {
private config: AuthConfig;
// Clock skew tolerance for expiry checks (30 seconds)
private readonly CLOCK_SKEW_MS = 30_000;
// Track if we've already warned about missing expiration to avoid spam
private hasWarnedAboutMissingExpiration = false;
private constructor(config?: Partial<AuthConfig>) {
this.config = getAuthConfig(config);
@@ -84,7 +86,11 @@ export class CredentialStore {
// Validate expiration time for tokens
if (expiresAtMs === undefined) {
this.logger.warn('No valid expiration time provided for token');
// Only log this warning once to avoid spam during auth flows
if (!this.hasWarnedAboutMissingExpiration) {
this.logger.warn('No valid expiration time provided for token');
this.hasWarnedAboutMissingExpiration = true;
}
return null;
}
@@ -174,6 +180,9 @@ export class CredentialStore {
mode: 0o600
});
fs.renameSync(tempFile, this.config.configFile);
// Reset the warning flag so it can be shown again for future invalid tokens
this.hasWarnedAboutMissingExpiration = false;
} catch (error) {
throw new AuthenticationError(
`Failed to save auth credentials: ${(error as Error).message}`,

View File

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

View File

@@ -29,13 +29,17 @@ export class SupabaseAuthClient {
*/
getClient(): SupabaseJSClient {
if (!this.client) {
// Get Supabase configuration from environment - using TM_PUBLIC prefix
const supabaseUrl = process.env.TM_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
// Get Supabase configuration from environment
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
const supabaseUrl =
process.env.TM_SUPABASE_URL || process.env.TM_PUBLIC_SUPABASE_URL;
const supabaseAnonKey =
process.env.TM_SUPABASE_ANON_KEY ||
process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new AuthenticationError(
'Supabase configuration missing. Please set TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY environment variables.',
'Supabase configuration missing. Please set TM_SUPABASE_URL and TM_SUPABASE_ANON_KEY (runtime) or TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY (build-time) environment variables.',
'CONFIG_MISSING'
);
}

View File

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

View File

@@ -358,11 +358,12 @@ export class ExportService {
tasks: any[]
): Promise<void> {
// Check if we should use the API endpoint or direct Supabase
const useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
const apiEndpoint =
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
if (useAPIEndpoint) {
if (apiEndpoint) {
// Use the new bulk import API endpoint
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks`;
const apiUrl = `${apiEndpoint}/ai/api/v1/briefs/${briefId}/tasks`;
// Transform tasks to flat structure for API
const flatTasks = this.transformTasksForBulkImport(tasks);

View File

@@ -27,6 +27,12 @@ export interface Brief {
status: string;
createdAt: string;
updatedAt: string;
document?: {
id: string;
title: string;
document_name: string;
description?: string;
};
}
/**
@@ -171,7 +177,12 @@ export class OrganizationService {
document_id,
status,
created_at,
updated_at
updated_at,
document:document_id (
id,
document_name,
title
)
`)
.eq('account_id', orgId);
@@ -196,7 +207,14 @@ export class OrganizationService {
documentId: brief.document_id,
status: brief.status,
createdAt: brief.created_at,
updatedAt: brief.updated_at
updatedAt: brief.updated_at,
document: brief.document
? {
id: brief.document.id,
document_name: brief.document.document_name,
title: brief.document.title
}
: undefined
}));
} catch (error) {
if (error instanceof TaskMasterError) {
@@ -224,7 +242,13 @@ export class OrganizationService {
document_id,
status,
created_at,
updated_at
updated_at,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('id', briefId)
.single();
@@ -253,7 +277,15 @@ export class OrganizationService {
documentId: briefData.document_id,
status: briefData.status,
createdAt: briefData.created_at,
updatedAt: briefData.updated_at
updatedAt: briefData.updated_at,
document: briefData.document
? {
id: briefData.document.id,
document_name: briefData.document.document_name,
title: briefData.document.title,
description: briefData.document.description
}
: undefined
};
} catch (error) {
if (error instanceof TaskMasterError) {

View File

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

View File

@@ -37,6 +37,13 @@ export interface ApiStorageConfig {
maxRetries?: number;
}
/**
* Auth context with a guaranteed briefId
*/
type ContextWithBrief = NonNullable<
ReturnType<typeof AuthManager.prototype.getContext>
> & { briefId: string };
/**
* ApiStorage implementation using repository pattern
* Provides flexibility to swap between different backend implementations
@@ -112,6 +119,13 @@ export class ApiStorage implements IStorage {
}
}
/**
* Get the storage type
*/
getType(): 'api' {
return 'api';
}
/**
* Load tags into cache
* In our API-based system, "tags" represent briefs
@@ -119,7 +133,7 @@ export class ApiStorage implements IStorage {
private async loadTagsIntoCache(): Promise<void> {
try {
const authManager = AuthManager.getInstance();
const context = await authManager.getContext();
const context = authManager.getContext();
// If we have a selected brief, create a virtual "tag" for it
if (context?.briefId) {
@@ -151,15 +165,7 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
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>'
);
}
const context = this.ensureBriefSelected('loadTasks');
// Load tasks from the current brief context with filters pushed to repository
const tasks = await this.retryOperation(() =>
@@ -174,12 +180,11 @@ export class ApiStorage implements IStorage {
return tasks;
} catch (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'
});
}
}
@@ -230,16 +235,17 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
this.ensureBriefSelected('loadTask');
return await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
} catch (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
});
}
}
@@ -318,7 +324,7 @@ export class ApiStorage implements IStorage {
try {
const authManager = AuthManager.getInstance();
const context = await authManager.getContext();
const context = authManager.getContext();
// In our API-based system, we only have one "tag" at a time - the current brief
if (context?.briefId) {
@@ -503,6 +509,8 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
this.ensureBriefSelected('updateTaskStatus');
const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
@@ -539,12 +547,12 @@ export class ApiStorage implements IStorage {
taskId
};
} catch (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
});
}
}
@@ -762,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
*/
@@ -780,4 +811,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<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

@@ -44,6 +44,13 @@ export class FileStorage implements IStorage {
await this.fileOps.cleanup();
}
/**
* Get the storage type
*/
getType(): 'file' {
return 'file';
}
/**
* Get statistics about the storage
*/

View File

@@ -82,8 +82,8 @@ export class StorageFactory {
apiAccessToken: credentials.token,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
process.env.TM_BASE_DOMAIN ||
process.env.TM_PUBLIC_BASE_DOMAIN
};
config.storage = nextStorage;
}
@@ -112,6 +112,7 @@ export class StorageFactory {
apiAccessToken: credentials.token,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.TM_BASE_DOMAIN ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};

View File

@@ -201,6 +201,44 @@ export class TaskMasterCore {
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
*/

View File

@@ -9,6 +9,8 @@
*/
import dotenv from 'dotenv';
// Load .env BEFORE any other imports to ensure env vars are available
dotenv.config();
// Add at the very beginning of the file
@@ -16,7 +18,8 @@ if (process.env.DEBUG === '1') {
console.error('DEBUG - dev.js received args:', process.argv.slice(2));
}
import { runCLI } from './modules/commands.js';
// Use dynamic import to ensure dotenv.config() runs before module-level code executes
const { runCLI } = await import('./modules/commands.js');
// Run the CLI with the process arguments
runCLI(process.argv);

View File

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