Compare commits

..

4 Commits

Author SHA1 Message Date
Ralph Khreish
93097dfeb5 chore: apply requested changes p3 2025-10-15 17:17:34 +02:00
Ralph Khreish
fa2b63de40 chore: apply requested changes p2 2025-10-15 17:01:56 +02:00
Ralph Khreish
b11dacece9 chore: apply requested PR changes and fix CI 2025-10-15 16:48:10 +02:00
Ralph Khreish
01cbbe97f2 fix: auth refresh 2025-10-15 15:28:32 +02:00
35 changed files with 321 additions and 1235 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": minor
---
Improve next command to work with remote

View File

@@ -22,7 +22,6 @@
"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

@@ -8,7 +8,6 @@ import { Command } from 'commander';
// Import all commands
import { ListTasksCommand } from './commands/list.command.js';
import { ShowCommand } from './commands/show.command.js';
import { NextCommand } from './commands/next.command.js';
import { AuthCommand } from './commands/auth.command.js';
import { ContextCommand } from './commands/context.command.js';
import { StartCommand } from './commands/start.command.js';
@@ -46,12 +45,6 @@ export class CommandRegistry {
commandClass: ShowCommand as any,
category: 'task'
},
{
name: 'next',
description: 'Find the next available task to work on',
commandClass: NextCommand as any,
category: 'task'
},
{
name: 'start',
description: 'Start working on a task with claude-code',

View File

@@ -14,8 +14,6 @@ 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
@@ -118,7 +116,8 @@ export class AuthCommand extends Command {
process.exit(0);
}, 100);
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -134,7 +133,8 @@ export class AuthCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -146,7 +146,8 @@ export class AuthCommand extends Command {
const result = this.displayStatus();
this.setLastResult(result);
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -162,7 +163,8 @@ export class AuthCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -349,37 +351,6 @@ 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',
@@ -387,7 +358,7 @@ export class AuthCommand extends Command {
message: 'Authentication successful'
};
} catch (error) {
displayError(error, { skipExit: true });
this.handleAuthError(error as AuthenticationError);
return {
success: false,
@@ -450,6 +421,51 @@ export class AuthCommand extends Command {
}
}
/**
* Handle authentication errors
*/
private handleAuthError(error: AuthenticationError): void {
console.error(chalk.red(`\n✗ ${error.message}`));
switch (error.code) {
case 'NETWORK_ERROR':
ui.displayWarning(
'Please check your internet connection and try again.'
);
break;
case 'INVALID_CREDENTIALS':
ui.displayWarning('Please check your credentials and try again.');
break;
case 'AUTH_EXPIRED':
ui.displayWarning(
'Your session has expired. Please authenticate again.'
);
break;
default:
if (process.env.DEBUG) {
console.error(chalk.gray(error.stack || ''));
}
}
}
/**
* Handle general errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
this.handleAuthError(error);
} else {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/**
* Set the last result for programmatic access
*/

View File

@@ -6,11 +6,13 @@
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora, { Ora } from 'ora';
import { AuthManager, type UserContext } from '@tm/core/auth';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/**
* Result type from context command
@@ -116,7 +118,8 @@ export class ContextCommand extends Command {
const result = this.displayContext();
this.setLastResult(result);
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -153,14 +156,10 @@ export class ContextCommand extends Command {
if (context.briefName || context.briefId) {
console.log(chalk.green('\n✓ Brief'));
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) {
if (context.briefName) {
console.log(chalk.white(` ${context.briefName}`));
} else if (context.briefId) {
}
if (context.briefId) {
console.log(chalk.gray(` ID: ${context.briefId}`));
}
}
@@ -212,7 +211,8 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -253,7 +253,6 @@ 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
@@ -300,7 +299,8 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -324,53 +324,25 @@ export class ContextCommand extends Command {
};
}
// 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];
// 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
}))
]
}
});
]);
if (selectedBrief) {
// Update context with brief
const briefName =
selectedBrief.document?.title ||
`Brief ${selectedBrief.id.slice(0, 8)}`;
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
this.authManager.updateContext({
briefId: selectedBrief.id,
briefName: briefName
@@ -382,7 +354,7 @@ export class ContextCommand extends Command {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
message: `Selected brief: ${selectedBrief.document?.title}`
message: `Selected brief: ${selectedBrief.name}`
};
} else {
// Clear brief selection
@@ -424,7 +396,8 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -470,7 +443,8 @@ export class ContextCommand extends Command {
process.exit(1);
}
} catch (error: any) {
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -494,7 +468,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_BASE_DOMAIN || 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_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
);
process.exit(1);
}
@@ -506,24 +480,20 @@ export class ContextCommand extends Command {
process.exit(1);
}
// Fetch org to get a friendly name and slug (optional)
// Fetch org to get a friendly name (optional)
let orgName: string | undefined;
let 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.document?.title || `Brief ${brief.id.slice(0, 8)}`;
const briefName = `Brief ${brief.id.slice(0, 8)}`;
this.authManager.updateContext({
orgId: brief.accountId,
orgName,
orgSlug,
briefId: brief.id,
briefName
});
@@ -545,7 +515,8 @@ export class ContextCommand extends Command {
try {
if (spinner?.isSpinning) spinner.stop();
} catch {}
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -674,6 +645,26 @@ export class ContextCommand extends Command {
}
}
/**
* Handle errors
*/
private handleError(error: any): void {
if (error instanceof AuthenticationError) {
console.error(chalk.red(`\n✗ ${error.message}`));
if (error.code === 'NOT_AUTHENTICATED') {
ui.displayWarning('Please authenticate first: tm auth login');
}
} else {
const msg = error?.message ?? String(error);
console.error(chalk.red(`Error: ${msg}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
}
}
/**
* Set the last result for programmatic access
*/
@@ -695,53 +686,6 @@ 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,10 +7,13 @@ import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora, { Ora } from 'ora';
import { AuthManager, type UserContext } from '@tm/core/auth';
import {
AuthManager,
AuthenticationError,
type UserContext
} from '@tm/core/auth';
import { TaskMasterCore, type ExportResult } from '@tm/core';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
/**
* Result type from export command
@@ -194,7 +197,8 @@ export class ExportCommand extends Command {
};
} catch (error: any) {
if (spinner?.isSpinning) spinner.fail('Export failed');
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -330,6 +334,26 @@ export class ExportCommand extends Command {
return confirmed;
}
/**
* 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,9 +17,8 @@ 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,
@@ -107,7 +106,14 @@ export class ListTasksCommand extends Command {
this.displayResults(result, options);
}
} catch (error: any) {
displayError(error);
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
@@ -251,12 +257,15 @@ export class ListTasksCommand extends Command {
* Display in text format with tables
*/
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
const { tasks, tag, storageType } = data;
const { tasks, tag } = data;
// Display header using utility function
displayCommandHeader(this.tmCore, {
// 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({
tag: tag || 'master',
storageType
filePath: filePath
});
// No tasks message

View File

@@ -1,248 +0,0 @@
/**
* @fileoverview NextCommand using Commander's native class pattern
* Extends Commander.Command for better integration with the framework
*/
import path from 'node:path';
import { Command } from 'commander';
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 { displayCommandHeader } from '../utils/display-helpers.js';
/**
* Options interface for the next command
*/
export interface NextCommandOptions {
tag?: string;
format?: 'text' | 'json';
silent?: boolean;
project?: string;
}
/**
* Result type from next command
*/
export interface NextTaskResult {
task: Task | null;
found: boolean;
tag: string;
storageType: Exclude<StorageType, 'auto'>;
}
/**
* NextCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core
*/
export class NextCommand extends Command {
private tmCore?: TaskMasterCore;
private lastResult?: NextTaskResult;
constructor(name?: string) {
super(name || 'next');
// Configure the command
this.description('Find the next available task to work on')
.option('-t, --tag <tag>', 'Filter by tag')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.option('--silent', 'Suppress output (useful for programmatic usage)')
.option('-p, --project <path>', 'Project root directory', process.cwd())
.action(async (options: NextCommandOptions) => {
await this.executeCommand(options);
});
}
/**
* Execute the next command
*/
private async executeCommand(options: NextCommandOptions): Promise<void> {
let hasError = false;
try {
// Validate options (throws on invalid options)
this.validateOptions(options);
// Initialize tm-core
await this.initializeCore(options.project || process.cwd());
// Get next task from core
const result = await this.getNextTask(options);
// Store result for programmatic access
this.setLastResult(result);
// Display results
if (!options.silent) {
this.displayResults(result, options);
}
} catch (error: any) {
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);
}
}
/**
* Validate command options
*/
private validateOptions(options: NextCommandOptions): void {
// Validate format
if (options.format && !['text', 'json'].includes(options.format)) {
throw new Error(
`Invalid format: ${options.format}. Valid formats are: text, json`
);
}
}
/**
* Initialize TaskMasterCore
*/
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
const resolved = path.resolve(projectRoot);
this.tmCore = await createTaskMasterCore({ projectPath: resolved });
}
}
/**
* Get next task from tm-core
*/
private async getNextTask(
options: NextCommandOptions
): Promise<NextTaskResult> {
if (!this.tmCore) {
throw new Error('TaskMasterCore not initialized');
}
// Call tm-core to get next task
const task = await this.tmCore.getNextTask(options.tag);
// Get storage type and active tag
const storageType = this.tmCore.getStorageType();
if (storageType === 'auto') {
throw new Error('Storage type must be resolved before use');
}
const activeTag = options.tag || this.tmCore.getActiveTag();
return {
task,
found: task !== null,
tag: activeTag,
storageType
};
}
/**
* Display results based on format
*/
private displayResults(
result: NextTaskResult,
options: NextCommandOptions
): void {
const format = options.format || 'text';
switch (format) {
case 'json':
this.displayJson(result);
break;
case 'text':
default:
this.displayText(result);
break;
}
}
/**
* Display in JSON format
*/
private displayJson(result: NextTaskResult): void {
console.log(JSON.stringify(result, null, 2));
}
/**
* Display in text format
*/
private displayText(result: NextTaskResult): void {
// Display header with storage info
displayCommandHeader(this.tmCore, {
tag: result.tag || 'master',
storageType: result.storageType
});
if (!result.found || !result.task) {
// No next task available
console.log(
boxen(
chalk.yellow(
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
),
{
padding: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '⚠ NO TASKS AVAILABLE ⚠',
titleAlignment: 'center'
}
)
);
console.log(
`\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}`
);
return;
}
const task = result.task;
// Display the task details using the same component as 'show' command
// with a custom header indicating this is the next task
const customHeader = `Next Task: #${task.id} - ${task.title}`;
displayTaskDetails(task, {
customHeader,
headerColor: 'green',
showSuggestedActions: true
});
}
/**
* Set the last result for programmatic access
*/
private setLastResult(result: NextTaskResult): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): NextTaskResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
if (this.tmCore) {
await this.tmCore.close();
this.tmCore = undefined;
}
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): NextCommand {
const nextCommand = new NextCommand(name);
program.addCommand(nextCommand);
return nextCommand;
}
}

View File

@@ -12,7 +12,6 @@ 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
@@ -86,7 +85,6 @@ export class SetStatusCommand extends Command {
private async executeCommand(
options: SetStatusCommandOptions
): Promise<void> {
let hasError = false;
try {
// Validate required options
if (!options.id) {
@@ -137,15 +135,16 @@ export class SetStatusCommand extends Command {
oldStatus: result.oldStatus,
newStatus: result.newStatus
});
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage = error?.getSanitizedDetails
? error.getSanitizedDetails().message
: error instanceof Error
? error.message
: String(error);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
if (!options.silent) {
console.error(
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
);
}
if (options.format === 'json') {
console.log(
JSON.stringify({
success: false,
@@ -154,13 +153,8 @@ 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 });
}
// Don't exit here - let finally block clean up first
break;
process.exit(1);
}
}
@@ -176,26 +170,25 @@ export class SetStatusCommand extends Command {
// Display results
this.displayResults(this.lastResult, options);
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
console.log(JSON.stringify({ success: false, error: errorMessage }));
} else if (!options.silent) {
displayError(error, { skipExit: true });
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
if (!options.silent) {
console.error(chalk.red(`Error: ${errorMessage}`));
}
if (options.format === 'json') {
console.log(JSON.stringify({ success: false, error: errorMessage }));
}
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,9 +9,7 @@ import boxen from 'boxen';
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
import type { StorageType } from '@tm/core/types';
import * as ui from '../utils/ui.js';
import { displayError } from '../utils/error-handler.js';
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayCommandHeader } from '../utils/display-helpers.js';
/**
* Options interface for the show command
@@ -114,7 +112,14 @@ export class ShowCommand extends Command {
this.displayResults(result, options);
}
} catch (error: any) {
displayError(error);
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
if (error.stack && process.env.DEBUG) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
}
@@ -252,15 +257,6 @@ export class ShowCommand extends Command {
return;
}
// 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,
@@ -275,12 +271,8 @@ export class ShowCommand extends Command {
result: ShowMultipleTasksResult,
_options: ShowCommandOptions
): void {
// Display header with storage info
const activeTag = this.tmCore?.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
// Header
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
@@ -299,6 +291,8 @@ export class ShowCommand extends Command {
showDependencies: true
})
);
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**

View File

@@ -16,7 +16,6 @@ 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
@@ -161,7 +160,8 @@ export class StartCommand extends Command {
if (spinner) {
spinner.fail('Operation failed');
}
displayError(error);
this.handleError(error);
process.exit(1);
}
}
@@ -452,6 +452,22 @@ export class StartCommand extends Command {
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**
* 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

@@ -6,7 +6,6 @@
// Commands
export { ListTasksCommand } from './commands/list.command.js';
export { ShowCommand } from './commands/show.command.js';
export { NextCommand } from './commands/next.command.js';
export { AuthCommand } from './commands/auth.command.js';
export { ContextCommand } from './commands/context.command.js';
export { StartCommand } from './commands/start.command.js';
@@ -24,9 +23,6 @@ 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,16 +5,6 @@
import chalk from 'chalk';
/**
* Brief information for API storage
*/
export interface BriefInfo {
briefId: string;
briefName: string;
orgSlug?: string;
webAppUrl?: string;
}
/**
* Header configuration options
*/
@@ -22,50 +12,22 @@ 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, storageType, briefInfo } = options;
const { filePath, tag } = options;
// 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
// Display tag and file path info
if (tag) {
let tagInfo = '';
if (tag && tag !== 'master') {
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
} else {
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
}
console.log(tagInfo);
@@ -77,5 +39,7 @@ export function displayHeader(options: HeaderOptions = {}): void {
: `${process.cwd()}/${filePath}`;
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
}
console.log(); // Empty line for spacing
}
}

View File

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

View File

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

View File

@@ -3,22 +3,6 @@ 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">

View File

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

View File

@@ -3,32 +3,7 @@ title: FAQ
sidebarTitle: "FAQ"
---
## 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.
Coming soon.
## 💬 Getting Help

View File

@@ -1,155 +0,0 @@
---
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,7 +104,6 @@
"name": "@tm/cli",
"license": "MIT",
"dependencies": {
"@inquirer/search": "^3.2.0",
"@tm/core": "*",
"boxen": "^8.0.1",
"chalk": "5.6.2",
@@ -125,91 +124,6 @@
"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,13 +7,11 @@ import path from 'path';
import { AuthConfig } from './types.js';
// Single base domain for all URLs
// 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
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsup's env option
// Default: https://tryhamster.com for production
const BASE_DOMAIN =
process.env.TM_BASE_DOMAIN || // Runtime override (for staging/tux)
process.env.TM_PUBLIC_BASE_DOMAIN; // Build-time (baked into compiled code)
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
'https://tryhamster.com';
/**
* Default authentication configuration
@@ -21,7 +19,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,8 +24,6 @@ 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);
@@ -86,11 +84,7 @@ export class CredentialStore {
// Validate expiration time for tokens
if (expiresAtMs === undefined) {
// 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;
}
this.logger.warn('No valid expiration time provided for token');
return null;
}
@@ -180,9 +174,6 @@ 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,7 +16,6 @@ export interface AuthCredentials {
export interface UserContext {
orgId?: string;
orgName?: string;
orgSlug?: string;
briefId?: string;
briefName?: string;
updatedAt: string;

View File

@@ -29,17 +29,13 @@ export class SupabaseAuthClient {
*/
getClient(): SupabaseJSClient {
if (!this.client) {
// 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;
// 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;
if (!supabaseUrl || !supabaseAnonKey) {
throw new AuthenticationError(
'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.',
'Supabase configuration missing. Please set TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY environment variables.',
'CONFIG_MISSING'
);
}

View File

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

View File

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

View File

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

View File

@@ -161,16 +161,6 @@ 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',
@@ -196,14 +186,6 @@ 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,
@@ -540,14 +522,6 @@ 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,13 +37,6 @@ 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
@@ -119,13 +112,6 @@ 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
@@ -133,7 +119,7 @@ export class ApiStorage implements IStorage {
private async loadTagsIntoCache(): Promise<void> {
try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
const context = await authManager.getContext();
// If we have a selected brief, create a virtual "tag" for it
if (context?.briefId) {
@@ -165,7 +151,15 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
const context = this.ensureBriefSelected('loadTasks');
const authManager = AuthManager.getInstance();
const context = await authManager.getContext();
// If no brief is selected in context, throw an error
if (!context?.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief <brief-id>'
);
}
// Load tasks from the current brief context with filters pushed to repository
const tasks = await this.retryOperation(() =>
@@ -180,11 +174,12 @@ export class ApiStorage implements IStorage {
return tasks;
} catch (error) {
this.wrapError(error, 'Failed to load tasks from API', {
operation: 'loadTasks',
tag,
context: 'brief-based loading'
});
throw new TaskMasterError(
'Failed to load tasks from API',
ERROR_CODES.STORAGE_ERROR,
{ operation: 'loadTasks', tag, context: 'brief-based loading' },
error as Error
);
}
}
@@ -235,17 +230,16 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
this.ensureBriefSelected('loadTask');
return await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
} catch (error) {
this.wrapError(error, 'Failed to load task from API', {
operation: 'loadTask',
taskId,
tag
});
throw new TaskMasterError(
'Failed to load task from API',
ERROR_CODES.STORAGE_ERROR,
{ operation: 'loadTask', taskId, tag },
error as Error
);
}
}
@@ -324,7 +318,7 @@ export class ApiStorage implements IStorage {
try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
const context = await authManager.getContext();
// In our API-based system, we only have one "tag" at a time - the current brief
if (context?.briefId) {
@@ -509,8 +503,6 @@ export class ApiStorage implements IStorage {
await this.ensureInitialized();
try {
this.ensureBriefSelected('updateTaskStatus');
const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
@@ -547,12 +539,12 @@ export class ApiStorage implements IStorage {
taskId
};
} catch (error) {
this.wrapError(error, 'Failed to update task status via API', {
operation: 'updateTaskStatus',
taskId,
newStatus,
tag
});
throw new TaskMasterError(
'Failed to update task status via API',
ERROR_CODES.STORAGE_ERROR,
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
error as Error
);
}
}
@@ -770,29 +762,6 @@ export class ApiStorage implements IStorage {
}
}
/**
* Ensure a brief is selected in the current context
* @returns The current auth context with a valid briefId
*/
private ensureBriefSelected(operation: string): ContextWithBrief {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
if (!context?.briefId) {
throw new TaskMasterError(
'No brief selected',
ERROR_CODES.NO_BRIEF_SELECTED,
{
operation,
userMessage:
'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
}
);
}
return context as ContextWithBrief;
}
/**
* Retry an operation with exponential backoff
*/
@@ -811,28 +780,4 @@ 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,13 +44,6 @@ 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_BASE_DOMAIN ||
process.env.TM_PUBLIC_BASE_DOMAIN
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};
config.storage = nextStorage;
}
@@ -112,7 +112,6 @@ 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,44 +201,6 @@ 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,8 +9,6 @@
*/
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
@@ -18,8 +16,7 @@ if (process.env.DEBUG === '1') {
console.error('DEBUG - dev.js received args:', process.argv.slice(2));
}
// Use dynamic import to ensure dotenv.config() runs before module-level code executes
const { runCLI } = await import('./modules/commands.js');
import { runCLI } from './modules/commands.js';
// Run the CLI with the process arguments
runCLI(process.argv);

View File

@@ -19,8 +19,7 @@ import {
registerAllCommands,
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification,
displayError
displayUpgradeNotification
} from '@tm/cli';
import {
@@ -2442,6 +2441,57 @@ ${result.result}
}
});
// next command
programInstance
.command('next')
.description(
`Show the next task to work on based on dependencies and status${chalk.reset('')}`
)
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-r, --report <report>',
'Path to the complexity report file',
COMPLEXITY_REPORT_FILE
)
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
const initOptions = {
tasksPath: options.file || true,
tag: options.tag
};
if (options.report && options.report !== COMPLEXITY_REPORT_FILE) {
initOptions.complexityReportPath = options.report;
}
// Initialize TaskMaster
const taskMaster = initTaskMaster({
tasksPath: options.file || true,
tag: options.tag,
complexityReportPath: options.report || false
});
const tag = taskMaster.getCurrentTag();
const context = {
projectRoot: taskMaster.getProjectRoot(),
tag
};
// Show current tag context
displayCurrentTagIndicator(tag);
await displayNextTask(
taskMaster.getTasksPath(),
taskMaster.getComplexityReportPath(),
context
);
});
// add-dependency command
programInstance
.command('add-dependency')
@@ -5157,7 +5207,10 @@ async function runCLI(argv = process.argv) {
);
} else {
// Generic error handling for other errors
displayError(error);
console.error(chalk.red(`Error: ${error.message}`));
if (getDebugFlag()) {
console.error(error);
}
}
process.exit(1);