Compare commits
4 Commits
docs/auto-
...
ralph/fix/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93097dfeb5 | ||
|
|
fa2b63de40 | ||
|
|
b11dacece9 | ||
|
|
01cbbe97f2 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Improve next command to work with remote
|
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
"test:ci": "vitest run --coverage --reporter=dot"
|
"test:ci": "vitest run --coverage --reporter=dot"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/search": "^3.2.0",
|
|
||||||
"@tm/core": "*",
|
"@tm/core": "*",
|
||||||
"boxen": "^8.0.1",
|
"boxen": "^8.0.1",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { Command } from 'commander';
|
|||||||
// Import all commands
|
// Import all commands
|
||||||
import { ListTasksCommand } from './commands/list.command.js';
|
import { ListTasksCommand } from './commands/list.command.js';
|
||||||
import { ShowCommand } from './commands/show.command.js';
|
import { ShowCommand } from './commands/show.command.js';
|
||||||
import { NextCommand } from './commands/next.command.js';
|
|
||||||
import { AuthCommand } from './commands/auth.command.js';
|
import { AuthCommand } from './commands/auth.command.js';
|
||||||
import { ContextCommand } from './commands/context.command.js';
|
import { ContextCommand } from './commands/context.command.js';
|
||||||
import { StartCommand } from './commands/start.command.js';
|
import { StartCommand } from './commands/start.command.js';
|
||||||
@@ -46,12 +45,6 @@ export class CommandRegistry {
|
|||||||
commandClass: ShowCommand as any,
|
commandClass: ShowCommand as any,
|
||||||
category: 'task'
|
category: 'task'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'next',
|
|
||||||
description: 'Find the next available task to work on',
|
|
||||||
commandClass: NextCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'start',
|
name: 'start',
|
||||||
description: 'Start working on a task with claude-code',
|
description: 'Start working on a task with claude-code',
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
type AuthCredentials
|
type AuthCredentials
|
||||||
} from '@tm/core/auth';
|
} from '@tm/core/auth';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { ContextCommand } from './context.command.js';
|
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result type from auth command
|
* Result type from auth command
|
||||||
@@ -118,7 +116,8 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +133,8 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,8 @@ export class AuthCommand extends Command {
|
|||||||
const result = this.displayStatus();
|
const result = this.displayStatus();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +163,8 @@ export class AuthCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,37 +351,6 @@ export class AuthCommand extends Command {
|
|||||||
chalk.gray(` Logged in as: ${credentials.email || credentials.userId}`)
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'login',
|
action: 'login',
|
||||||
@@ -387,7 +358,7 @@ export class AuthCommand extends Command {
|
|||||||
message: 'Authentication successful'
|
message: 'Authentication successful'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
displayError(error, { skipExit: true });
|
this.handleAuthError(error as AuthenticationError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -450,6 +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
|
* Set the last result for programmatic access
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,11 +6,13 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import search from '@inquirer/search';
|
|
||||||
import ora, { Ora } from 'ora';
|
import ora, { Ora } from 'ora';
|
||||||
import { AuthManager, type UserContext } from '@tm/core/auth';
|
import {
|
||||||
|
AuthManager,
|
||||||
|
AuthenticationError,
|
||||||
|
type UserContext
|
||||||
|
} from '@tm/core/auth';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result type from context command
|
* Result type from context command
|
||||||
@@ -116,7 +118,8 @@ export class ContextCommand extends Command {
|
|||||||
const result = this.displayContext();
|
const result = this.displayContext();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +156,10 @@ export class ContextCommand extends Command {
|
|||||||
|
|
||||||
if (context.briefName || context.briefId) {
|
if (context.briefName || context.briefId) {
|
||||||
console.log(chalk.green('\n✓ Brief'));
|
console.log(chalk.green('\n✓ Brief'));
|
||||||
if (context.briefName && context.briefId) {
|
if (context.briefName) {
|
||||||
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}`));
|
console.log(chalk.white(` ${context.briefName}`));
|
||||||
} else if (context.briefId) {
|
}
|
||||||
|
if (context.briefId) {
|
||||||
console.log(chalk.gray(` ID: ${context.briefId}`));
|
console.log(chalk.gray(` ID: ${context.briefId}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,7 +211,8 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +253,6 @@ export class ContextCommand extends Command {
|
|||||||
this.authManager.updateContext({
|
this.authManager.updateContext({
|
||||||
orgId: selectedOrg.id,
|
orgId: selectedOrg.id,
|
||||||
orgName: selectedOrg.name,
|
orgName: selectedOrg.name,
|
||||||
orgSlug: selectedOrg.slug,
|
|
||||||
// Clear brief when changing org
|
// Clear brief when changing org
|
||||||
briefId: undefined,
|
briefId: undefined,
|
||||||
briefName: undefined
|
briefName: undefined
|
||||||
@@ -300,7 +299,8 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,53 +324,25 @@ export class ContextCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for selection with search
|
// Prompt for selection
|
||||||
const selectedBrief = await search<(typeof briefs)[0] | null>({
|
const { selectedBrief } = await inquirer.prompt([
|
||||||
message: 'Search for a brief:',
|
{
|
||||||
source: async (input) => {
|
type: 'list',
|
||||||
const searchTerm = input?.toLowerCase() || '';
|
name: 'selectedBrief',
|
||||||
|
message: 'Select a brief:',
|
||||||
// Static option for no brief
|
choices: [
|
||||||
const noBriefOption = {
|
{ name: '(No brief - organization level)', value: null },
|
||||||
name: '(No brief - organization level)',
|
...briefs.map((brief) => ({
|
||||||
value: null as any,
|
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
||||||
description: 'Clear brief selection'
|
value: brief
|
||||||
};
|
}))
|
||||||
|
]
|
||||||
// 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) {
|
if (selectedBrief) {
|
||||||
// Update context with brief
|
// Update context with brief
|
||||||
const briefName =
|
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
|
||||||
selectedBrief.document?.title ||
|
|
||||||
`Brief ${selectedBrief.id.slice(0, 8)}`;
|
|
||||||
this.authManager.updateContext({
|
this.authManager.updateContext({
|
||||||
briefId: selectedBrief.id,
|
briefId: selectedBrief.id,
|
||||||
briefName: briefName
|
briefName: briefName
|
||||||
@@ -382,7 +354,7 @@ export class ContextCommand extends Command {
|
|||||||
success: true,
|
success: true,
|
||||||
action: 'select-brief',
|
action: 'select-brief',
|
||||||
context: this.authManager.getContext() || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: `Selected brief: ${selectedBrief.document?.title}`
|
message: `Selected brief: ${selectedBrief.name}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Clear brief selection
|
// Clear brief selection
|
||||||
@@ -424,7 +396,8 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +443,8 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,7 +468,7 @@ export class ContextCommand extends Command {
|
|||||||
if (!briefId) {
|
if (!briefId) {
|
||||||
spinner.fail('Could not extract a brief ID from the provided input');
|
spinner.fail('Could not extract a brief ID from the provided input');
|
||||||
ui.displayError(
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -506,24 +480,20 @@ export class ContextCommand extends Command {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch org to get a friendly name and slug (optional)
|
// Fetch org to get a friendly name (optional)
|
||||||
let orgName: string | undefined;
|
let orgName: string | undefined;
|
||||||
let orgSlug: string | undefined;
|
|
||||||
try {
|
try {
|
||||||
const org = await this.authManager.getOrganization(brief.accountId);
|
const org = await this.authManager.getOrganization(brief.accountId);
|
||||||
orgName = org?.name;
|
orgName = org?.name;
|
||||||
orgSlug = org?.slug;
|
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal if org lookup fails
|
// Non-fatal if org lookup fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update context: set org and brief
|
// Update context: set org and brief
|
||||||
const briefName =
|
const briefName = `Brief ${brief.id.slice(0, 8)}`;
|
||||||
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
|
|
||||||
this.authManager.updateContext({
|
this.authManager.updateContext({
|
||||||
orgId: brief.accountId,
|
orgId: brief.accountId,
|
||||||
orgName,
|
orgName,
|
||||||
orgSlug,
|
|
||||||
briefId: brief.id,
|
briefId: brief.id,
|
||||||
briefName
|
briefName
|
||||||
});
|
});
|
||||||
@@ -545,7 +515,8 @@ export class ContextCommand extends Command {
|
|||||||
try {
|
try {
|
||||||
if (spinner?.isSpinning) spinner.stop();
|
if (spinner?.isSpinning) spinner.stop();
|
||||||
} catch {}
|
} catch {}
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,6 +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
|
* Set the last result for programmatic access
|
||||||
*/
|
*/
|
||||||
@@ -695,53 +686,6 @@ export class ContextCommand extends Command {
|
|||||||
return this.authManager.getContext();
|
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
|
* Clean up resources
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import { Command } from 'commander';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import ora, { Ora } from 'ora';
|
import ora, { Ora } from 'ora';
|
||||||
import { AuthManager, type UserContext } from '@tm/core/auth';
|
import {
|
||||||
|
AuthManager,
|
||||||
|
AuthenticationError,
|
||||||
|
type UserContext
|
||||||
|
} from '@tm/core/auth';
|
||||||
import { TaskMasterCore, type ExportResult } from '@tm/core';
|
import { TaskMasterCore, type ExportResult } from '@tm/core';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result type from export command
|
* Result type from export command
|
||||||
@@ -194,7 +197,8 @@ export class ExportCommand extends Command {
|
|||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (spinner?.isSpinning) spinner.fail('Export failed');
|
if (spinner?.isSpinning) spinner.fail('Export failed');
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +334,26 @@ export class ExportCommand extends Command {
|
|||||||
return confirmed;
|
return confirmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle errors
|
||||||
|
*/
|
||||||
|
private handleError(error: any): void {
|
||||||
|
if (error instanceof AuthenticationError) {
|
||||||
|
console.error(chalk.red(`\n✗ ${error.message}`));
|
||||||
|
|
||||||
|
if (error.code === 'NOT_AUTHENTICATED') {
|
||||||
|
ui.displayWarning('Please authenticate first: tm auth login');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const msg = error?.message ?? String(error);
|
||||||
|
console.error(chalk.red(`Error: ${msg}`));
|
||||||
|
|
||||||
|
if (error.stack && process.env.DEBUG) {
|
||||||
|
console.error(chalk.gray(error.stack));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last export result (useful for testing)
|
* Get the last export result (useful for testing)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
} from '@tm/core';
|
} from '@tm/core';
|
||||||
import type { StorageType } from '@tm/core/types';
|
import type { StorageType } from '@tm/core/types';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
import { displayCommandHeader } from '../utils/display-helpers.js';
|
|
||||||
import {
|
import {
|
||||||
|
displayHeader,
|
||||||
displayDashboards,
|
displayDashboards,
|
||||||
calculateTaskStatistics,
|
calculateTaskStatistics,
|
||||||
calculateSubtaskStatistics,
|
calculateSubtaskStatistics,
|
||||||
@@ -107,7 +106,14 @@ export class ListTasksCommand extends Command {
|
|||||||
this.displayResults(result, options);
|
this.displayResults(result, options);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
const msg = error?.getSanitizedDetails?.() ?? {
|
||||||
|
message: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||||
|
if (error.stack && process.env.DEBUG) {
|
||||||
|
console.error(chalk.gray(error.stack));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,12 +257,15 @@ export class ListTasksCommand extends Command {
|
|||||||
* Display in text format with tables
|
* Display in text format with tables
|
||||||
*/
|
*/
|
||||||
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
|
private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
|
||||||
const { tasks, tag, storageType } = data;
|
const { tasks, tag } = data;
|
||||||
|
|
||||||
// Display header using utility function
|
// Get file path for display
|
||||||
displayCommandHeader(this.tmCore, {
|
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
|
||||||
|
|
||||||
|
// Display header without banner (banner already shown by main CLI)
|
||||||
|
displayHeader({
|
||||||
tag: tag || 'master',
|
tag: tag || 'master',
|
||||||
storageType
|
filePath: filePath
|
||||||
});
|
});
|
||||||
|
|
||||||
// No tasks message
|
// No tasks message
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
type TaskStatus
|
type TaskStatus
|
||||||
} from '@tm/core';
|
} from '@tm/core';
|
||||||
import type { StorageType } from '@tm/core/types';
|
import type { StorageType } from '@tm/core/types';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid task status values for validation
|
* Valid task status values for validation
|
||||||
@@ -86,7 +85,6 @@ export class SetStatusCommand extends Command {
|
|||||||
private async executeCommand(
|
private async executeCommand(
|
||||||
options: SetStatusCommandOptions
|
options: SetStatusCommandOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let hasError = false;
|
|
||||||
try {
|
try {
|
||||||
// Validate required options
|
// Validate required options
|
||||||
if (!options.id) {
|
if (!options.id) {
|
||||||
@@ -137,15 +135,16 @@ export class SetStatusCommand extends Command {
|
|||||||
oldStatus: result.oldStatus,
|
oldStatus: result.oldStatus,
|
||||||
newStatus: result.newStatus
|
newStatus: result.newStatus
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
hasError = true;
|
const errorMessage =
|
||||||
if (options.format === 'json') {
|
error instanceof Error ? error.message : String(error);
|
||||||
const errorMessage = error?.getSanitizedDetails
|
|
||||||
? error.getSanitizedDetails().message
|
|
||||||
: error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: String(error);
|
|
||||||
|
|
||||||
|
if (!options.silent) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(`Failed to update task ${taskId}: ${errorMessage}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (options.format === 'json') {
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -154,13 +153,8 @@ export class SetStatusCommand extends Command {
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (!options.silent) {
|
|
||||||
// Show which task failed with context
|
|
||||||
console.error(chalk.red(`\nFailed to update task ${taskId}:`));
|
|
||||||
displayError(error, { skipExit: true });
|
|
||||||
}
|
}
|
||||||
// Don't exit here - let finally block clean up first
|
process.exit(1);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,26 +170,25 @@ export class SetStatusCommand extends Command {
|
|||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
this.displayResults(this.lastResult, options);
|
this.displayResults(this.lastResult, options);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
hasError = true;
|
const errorMessage =
|
||||||
if (options.format === 'json') {
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
if (!options.silent) {
|
||||||
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
console.error(chalk.red(`Error: ${errorMessage}`));
|
||||||
} else if (!options.silent) {
|
|
||||||
displayError(error, { skipExit: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.format === 'json') {
|
||||||
|
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up resources
|
// Clean up resources
|
||||||
if (this.tmCore) {
|
if (this.tmCore) {
|
||||||
await this.tmCore.close();
|
await this.tmCore.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit after cleanup completes
|
|
||||||
if (hasError) {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import boxen from 'boxen';
|
|||||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||||
import type { StorageType } from '@tm/core/types';
|
import type { StorageType } from '@tm/core/types';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||||
import { displayCommandHeader } from '../utils/display-helpers.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options interface for the show command
|
* Options interface for the show command
|
||||||
@@ -114,7 +112,14 @@ export class ShowCommand extends Command {
|
|||||||
this.displayResults(result, options);
|
this.displayResults(result, options);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
displayError(error);
|
const msg = error?.getSanitizedDetails?.() ?? {
|
||||||
|
message: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||||
|
if (error.stack && process.env.DEBUG) {
|
||||||
|
console.error(chalk.gray(error.stack));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,15 +257,6 @@ export class ShowCommand extends Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display header with storage info
|
|
||||||
const activeTag = this.tmCore?.getActiveTag() || 'master';
|
|
||||||
displayCommandHeader(this.tmCore, {
|
|
||||||
tag: activeTag,
|
|
||||||
storageType: result.storageType
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(); // Add spacing
|
|
||||||
|
|
||||||
// Use the global task details display function
|
// Use the global task details display function
|
||||||
displayTaskDetails(result.task, {
|
displayTaskDetails(result.task, {
|
||||||
statusFilter: options.status,
|
statusFilter: options.status,
|
||||||
@@ -275,12 +271,8 @@ export class ShowCommand extends Command {
|
|||||||
result: ShowMultipleTasksResult,
|
result: ShowMultipleTasksResult,
|
||||||
_options: ShowCommandOptions
|
_options: ShowCommandOptions
|
||||||
): void {
|
): void {
|
||||||
// Display header with storage info
|
// Header
|
||||||
const activeTag = this.tmCore?.getActiveTag() || 'master';
|
ui.displayBanner(`Tasks (${result.tasks.length} found)`);
|
||||||
displayCommandHeader(this.tmCore, {
|
|
||||||
tag: activeTag,
|
|
||||||
storageType: result.storageType
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.notFound.length > 0) {
|
if (result.notFound.length > 0) {
|
||||||
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
|
console.log(chalk.yellow(`\n⚠ Not found: ${result.notFound.join(', ')}`));
|
||||||
@@ -299,6 +291,8 @@ export class ShowCommand extends Command {
|
|||||||
showDependencies: true
|
showDependencies: true
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from '@tm/core';
|
} from '@tm/core';
|
||||||
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
|
||||||
import * as ui from '../utils/ui.js';
|
import * as ui from '../utils/ui.js';
|
||||||
import { displayError } from '../utils/error-handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI-specific options interface for the start command
|
* CLI-specific options interface for the start command
|
||||||
@@ -161,7 +160,8 @@ export class StartCommand extends Command {
|
|||||||
if (spinner) {
|
if (spinner) {
|
||||||
spinner.fail('Operation failed');
|
spinner.fail('Operation failed');
|
||||||
}
|
}
|
||||||
displayError(error);
|
this.handleError(error);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +452,22 @@ export class StartCommand extends Command {
|
|||||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle general errors
|
||||||
|
*/
|
||||||
|
private handleError(error: any): void {
|
||||||
|
const msg = error?.getSanitizedDetails?.() ?? {
|
||||||
|
message: error?.message ?? String(error)
|
||||||
|
};
|
||||||
|
console.error(chalk.red(`Error: ${msg.message || 'Unexpected error'}`));
|
||||||
|
|
||||||
|
// Show stack trace in development mode or when DEBUG is set
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
if ((isDevelopment || process.env.DEBUG) && error.stack) {
|
||||||
|
console.error(chalk.gray(error.stack));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the last result for programmatic access
|
* Set the last result for programmatic access
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Commands
|
// Commands
|
||||||
export { ListTasksCommand } from './commands/list.command.js';
|
export { ListTasksCommand } from './commands/list.command.js';
|
||||||
export { ShowCommand } from './commands/show.command.js';
|
export { ShowCommand } from './commands/show.command.js';
|
||||||
export { NextCommand } from './commands/next.command.js';
|
|
||||||
export { AuthCommand } from './commands/auth.command.js';
|
export { AuthCommand } from './commands/auth.command.js';
|
||||||
export { ContextCommand } from './commands/context.command.js';
|
export { ContextCommand } from './commands/context.command.js';
|
||||||
export { StartCommand } from './commands/start.command.js';
|
export { StartCommand } from './commands/start.command.js';
|
||||||
@@ -24,9 +23,6 @@ export {
|
|||||||
// UI utilities (for other commands to use)
|
// UI utilities (for other commands to use)
|
||||||
export * as ui from './utils/ui.js';
|
export * as ui from './utils/ui.js';
|
||||||
|
|
||||||
// Error handling utilities
|
|
||||||
export { displayError, isDebugMode } from './utils/error-handler.js';
|
|
||||||
|
|
||||||
// Auto-update utilities
|
// Auto-update utilities
|
||||||
export {
|
export {
|
||||||
checkForUpdate,
|
checkForUpdate,
|
||||||
|
|||||||
@@ -5,16 +5,6 @@
|
|||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
/**
|
|
||||||
* Brief information for API storage
|
|
||||||
*/
|
|
||||||
export interface BriefInfo {
|
|
||||||
briefId: string;
|
|
||||||
briefName: string;
|
|
||||||
orgSlug?: string;
|
|
||||||
webAppUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header configuration options
|
* Header configuration options
|
||||||
*/
|
*/
|
||||||
@@ -22,50 +12,22 @@ export interface HeaderOptions {
|
|||||||
title?: string;
|
title?: string;
|
||||||
tag?: string;
|
tag?: string;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
storageType?: 'api' | 'file';
|
|
||||||
briefInfo?: BriefInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the Task Master header with project info
|
* Display the Task Master header with project info
|
||||||
*/
|
*/
|
||||||
export function displayHeader(options: HeaderOptions = {}): void {
|
export function displayHeader(options: HeaderOptions = {}): void {
|
||||||
const { filePath, tag, storageType, briefInfo } = options;
|
const { filePath, tag } = options;
|
||||||
|
|
||||||
// Display different header based on storage type
|
// Display tag and file path info
|
||||||
if (storageType === 'api' && briefInfo) {
|
if (tag) {
|
||||||
// API storage: Show brief information
|
|
||||||
const briefDisplay = `🏷 Brief: ${chalk.cyan(briefInfo.briefName)} ${chalk.gray(`(${briefInfo.briefId})`)}`;
|
|
||||||
console.log(briefDisplay);
|
|
||||||
|
|
||||||
// Construct and display the brief URL or ID
|
|
||||||
if (briefInfo.webAppUrl && briefInfo.orgSlug) {
|
|
||||||
const briefUrl = `${briefInfo.webAppUrl}/home/${briefInfo.orgSlug}/briefs/${briefInfo.briefId}/plan`;
|
|
||||||
console.log(`Listing tasks from: ${chalk.dim(briefUrl)}`);
|
|
||||||
} else if (briefInfo.webAppUrl) {
|
|
||||||
// Show web app URL and brief ID if org slug is missing
|
|
||||||
console.log(
|
|
||||||
`Listing tasks from: ${chalk.dim(`${briefInfo.webAppUrl} (Brief: ${briefInfo.briefId})`)}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`💡 Tip: Run ${chalk.cyan('tm context select')} to set your organization and see the full URL`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Fallback: just show the brief ID if we can't get web app URL
|
|
||||||
console.log(
|
|
||||||
`Listing tasks from: ${chalk.dim(`API (Brief ID: ${briefInfo.briefId})`)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (tag) {
|
|
||||||
// File storage: Show tag information
|
|
||||||
let tagInfo = '';
|
let tagInfo = '';
|
||||||
|
|
||||||
if (tag && tag !== 'master') {
|
if (tag && tag !== 'master') {
|
||||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||||
} else {
|
} else {
|
||||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(tagInfo);
|
console.log(tagInfo);
|
||||||
@@ -77,5 +39,7 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
|||||||
: `${process.cwd()}/${filePath}`;
|
: `${process.cwd()}/${filePath}`;
|
||||||
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(); // Empty line for spacing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,22 +3,6 @@ title: CLI Commands
|
|||||||
sidebarTitle: "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>
|
<AccordionGroup>
|
||||||
<Accordion title="Parse PRD">
|
<Accordion title="Parse PRD">
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"getting-started/api-keys",
|
"getting-started/api-keys",
|
||||||
"getting-started/troubleshooting",
|
|
||||||
"getting-started/faq",
|
"getting-started/faq",
|
||||||
"getting-started/contribute"
|
"getting-started/contribute"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,32 +3,7 @@ title: FAQ
|
|||||||
sidebarTitle: "FAQ"
|
sidebarTitle: "FAQ"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Common Questions
|
Coming soon.
|
||||||
|
|
||||||
### 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
|
## 💬 Getting Help
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
62
output.txt
62
output.txt
File diff suppressed because one or more lines are too long
86
package-lock.json
generated
86
package-lock.json
generated
@@ -104,7 +104,6 @@
|
|||||||
"name": "@tm/cli",
|
"name": "@tm/cli",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/search": "^3.2.0",
|
|
||||||
"@tm/core": "*",
|
"@tm/core": "*",
|
||||||
"boxen": "^8.0.1",
|
"boxen": "^8.0.1",
|
||||||
"chalk": "5.6.2",
|
"chalk": "5.6.2",
|
||||||
@@ -125,91 +124,6 @@
|
|||||||
"node": ">=18.0.0"
|
"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": {
|
"apps/docs": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ import path from 'path';
|
|||||||
import { AuthConfig } from './types.js';
|
import { AuthConfig } from './types.js';
|
||||||
|
|
||||||
// Single base domain for all URLs
|
// 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 tsup's env option
|
||||||
// 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
|
// Default: https://tryhamster.com for production
|
||||||
const BASE_DOMAIN =
|
const BASE_DOMAIN =
|
||||||
process.env.TM_BASE_DOMAIN || // Runtime override (for staging/tux)
|
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
|
||||||
process.env.TM_PUBLIC_BASE_DOMAIN; // Build-time (baked into compiled code)
|
'https://tryhamster.com';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default authentication configuration
|
* Default authentication configuration
|
||||||
@@ -21,7 +19,7 @@ const BASE_DOMAIN =
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
export const DEFAULT_AUTH_CONFIG: AuthConfig = {
|
||||||
// Base domain for all services
|
// Base domain for all services
|
||||||
baseUrl: BASE_DOMAIN!,
|
baseUrl: BASE_DOMAIN,
|
||||||
|
|
||||||
// Configuration directory and file paths
|
// Configuration directory and file paths
|
||||||
configDir: path.join(os.homedir(), '.taskmaster'),
|
configDir: path.join(os.homedir(), '.taskmaster'),
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ export class CredentialStore {
|
|||||||
private config: AuthConfig;
|
private config: AuthConfig;
|
||||||
// Clock skew tolerance for expiry checks (30 seconds)
|
// Clock skew tolerance for expiry checks (30 seconds)
|
||||||
private readonly CLOCK_SKEW_MS = 30_000;
|
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>) {
|
private constructor(config?: Partial<AuthConfig>) {
|
||||||
this.config = getAuthConfig(config);
|
this.config = getAuthConfig(config);
|
||||||
@@ -86,11 +84,7 @@ export class CredentialStore {
|
|||||||
|
|
||||||
// Validate expiration time for tokens
|
// Validate expiration time for tokens
|
||||||
if (expiresAtMs === undefined) {
|
if (expiresAtMs === undefined) {
|
||||||
// Only log this warning once to avoid spam during auth flows
|
this.logger.warn('No valid expiration time provided for token');
|
||||||
if (!this.hasWarnedAboutMissingExpiration) {
|
|
||||||
this.logger.warn('No valid expiration time provided for token');
|
|
||||||
this.hasWarnedAboutMissingExpiration = true;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +174,6 @@ export class CredentialStore {
|
|||||||
mode: 0o600
|
mode: 0o600
|
||||||
});
|
});
|
||||||
fs.renameSync(tempFile, this.config.configFile);
|
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) {
|
} catch (error) {
|
||||||
throw new AuthenticationError(
|
throw new AuthenticationError(
|
||||||
`Failed to save auth credentials: ${(error as Error).message}`,
|
`Failed to save auth credentials: ${(error as Error).message}`,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface AuthCredentials {
|
|||||||
export interface UserContext {
|
export interface UserContext {
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
orgName?: string;
|
orgName?: string;
|
||||||
orgSlug?: string;
|
|
||||||
briefId?: string;
|
briefId?: string;
|
||||||
briefName?: string;
|
briefName?: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -29,17 +29,13 @@ export class SupabaseAuthClient {
|
|||||||
*/
|
*/
|
||||||
getClient(): SupabaseJSClient {
|
getClient(): SupabaseJSClient {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
// Get Supabase configuration from environment
|
// Get Supabase configuration from environment - using TM_PUBLIC prefix
|
||||||
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
|
const supabaseUrl = process.env.TM_PUBLIC_SUPABASE_URL;
|
||||||
const supabaseUrl =
|
const supabaseAnonKey = process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
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) {
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
throw new AuthenticationError(
|
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'
|
'CONFIG_MISSING'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,7 @@ export const ERROR_CODES = {
|
|||||||
INVALID_INPUT: 'INVALID_INPUT',
|
INVALID_INPUT: 'INVALID_INPUT',
|
||||||
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
|
NOT_IMPLEMENTED: 'NOT_IMPLEMENTED',
|
||||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
||||||
NOT_FOUND: 'NOT_FOUND',
|
NOT_FOUND: 'NOT_FOUND'
|
||||||
|
|
||||||
// Context errors
|
|
||||||
NO_BRIEF_SELECTED: 'NO_BRIEF_SELECTED'
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||||
|
|||||||
@@ -358,12 +358,11 @@ export class ExportService {
|
|||||||
tasks: any[]
|
tasks: any[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Check if we should use the API endpoint or direct Supabase
|
// Check if we should use the API endpoint or direct Supabase
|
||||||
const apiEndpoint =
|
const useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
|
||||||
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
|
|
||||||
|
|
||||||
if (apiEndpoint) {
|
if (useAPIEndpoint) {
|
||||||
// Use the new bulk import API endpoint
|
// 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
|
// Transform tasks to flat structure for API
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||||
|
|||||||
@@ -27,12 +27,6 @@ export interface Brief {
|
|||||||
status: string;
|
status: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
document?: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
document_name: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,12 +171,7 @@ export class OrganizationService {
|
|||||||
document_id,
|
document_id,
|
||||||
status,
|
status,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at
|
||||||
document:document_id (
|
|
||||||
id,
|
|
||||||
document_name,
|
|
||||||
title
|
|
||||||
)
|
|
||||||
`)
|
`)
|
||||||
.eq('account_id', orgId);
|
.eq('account_id', orgId);
|
||||||
|
|
||||||
@@ -207,14 +196,7 @@ export class OrganizationService {
|
|||||||
documentId: brief.document_id,
|
documentId: brief.document_id,
|
||||||
status: brief.status,
|
status: brief.status,
|
||||||
createdAt: brief.created_at,
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof TaskMasterError) {
|
if (error instanceof TaskMasterError) {
|
||||||
@@ -242,13 +224,7 @@ export class OrganizationService {
|
|||||||
document_id,
|
document_id,
|
||||||
status,
|
status,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at
|
||||||
document:document_id (
|
|
||||||
id,
|
|
||||||
document_name,
|
|
||||||
title,
|
|
||||||
description
|
|
||||||
)
|
|
||||||
`)
|
`)
|
||||||
.eq('id', briefId)
|
.eq('id', briefId)
|
||||||
.single();
|
.single();
|
||||||
@@ -277,15 +253,7 @@ export class OrganizationService {
|
|||||||
documentId: briefData.document_id,
|
documentId: briefData.document_id,
|
||||||
status: briefData.status,
|
status: briefData.status,
|
||||||
createdAt: briefData.created_at,
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof TaskMasterError) {
|
if (error instanceof TaskMasterError) {
|
||||||
|
|||||||
@@ -161,16 +161,6 @@ export class TaskService {
|
|||||||
storageType: this.getStorageType()
|
storageType: this.getStorageType()
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
|
|
||||||
if (
|
|
||||||
error instanceof TaskMasterError &&
|
|
||||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
|
||||||
) {
|
|
||||||
// Just re-throw user-facing errors without wrapping
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log internal errors
|
|
||||||
this.logger.error('Failed to get task list', error);
|
this.logger.error('Failed to get task list', error);
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
'Failed to get task list',
|
'Failed to get task list',
|
||||||
@@ -196,14 +186,6 @@ export class TaskService {
|
|||||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
||||||
return await this.storage.loadTask(String(taskId), activeTag);
|
return await this.storage.loadTask(String(taskId), activeTag);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
|
||||||
if (
|
|
||||||
error instanceof TaskMasterError &&
|
|
||||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
|
||||||
) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
`Failed to get task ${taskId}`,
|
`Failed to get task ${taskId}`,
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
@@ -540,14 +522,6 @@ export class TaskService {
|
|||||||
activeTag
|
activeTag
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
|
|
||||||
if (
|
|
||||||
error instanceof TaskMasterError &&
|
|
||||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
|
||||||
) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
`Failed to update task status for ${taskIdStr}`,
|
`Failed to update task status for ${taskIdStr}`,
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
|
|||||||
@@ -37,13 +37,6 @@ export interface ApiStorageConfig {
|
|||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Auth context with a guaranteed briefId
|
|
||||||
*/
|
|
||||||
type ContextWithBrief = NonNullable<
|
|
||||||
ReturnType<typeof AuthManager.prototype.getContext>
|
|
||||||
> & { briefId: string };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ApiStorage implementation using repository pattern
|
* ApiStorage implementation using repository pattern
|
||||||
* Provides flexibility to swap between different backend implementations
|
* Provides flexibility to swap between different backend implementations
|
||||||
@@ -119,13 +112,6 @@ export class ApiStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the storage type
|
|
||||||
*/
|
|
||||||
getType(): 'api' {
|
|
||||||
return 'api';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load tags into cache
|
* Load tags into cache
|
||||||
* In our API-based system, "tags" represent briefs
|
* In our API-based system, "tags" represent briefs
|
||||||
@@ -133,7 +119,7 @@ export class ApiStorage implements IStorage {
|
|||||||
private async loadTagsIntoCache(): Promise<void> {
|
private async loadTagsIntoCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
// If we have a selected brief, create a virtual "tag" for it
|
// If we have a selected brief, create a virtual "tag" for it
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
@@ -165,7 +151,15 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const context = this.ensureBriefSelected('loadTasks');
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
|
// If no brief is selected in context, throw an error
|
||||||
|
if (!context?.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief <brief-id>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Load tasks from the current brief context with filters pushed to repository
|
// Load tasks from the current brief context with filters pushed to repository
|
||||||
const tasks = await this.retryOperation(() =>
|
const tasks = await this.retryOperation(() =>
|
||||||
@@ -180,11 +174,12 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
return tasks;
|
return tasks;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.wrapError(error, 'Failed to load tasks from API', {
|
throw new TaskMasterError(
|
||||||
operation: 'loadTasks',
|
'Failed to load tasks from API',
|
||||||
tag,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
context: 'brief-based loading'
|
{ operation: 'loadTasks', tag, context: 'brief-based loading' },
|
||||||
});
|
error as Error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,17 +230,16 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ensureBriefSelected('loadTask');
|
|
||||||
|
|
||||||
return await this.retryOperation(() =>
|
return await this.retryOperation(() =>
|
||||||
this.repository.getTask(this.projectId, taskId)
|
this.repository.getTask(this.projectId, taskId)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.wrapError(error, 'Failed to load task from API', {
|
throw new TaskMasterError(
|
||||||
operation: 'loadTask',
|
'Failed to load task from API',
|
||||||
taskId,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
tag
|
{ operation: 'loadTask', taskId, tag },
|
||||||
});
|
error as Error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +318,7 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
const context = await authManager.getContext();
|
||||||
|
|
||||||
// In our API-based system, we only have one "tag" at a time - the current brief
|
// In our API-based system, we only have one "tag" at a time - the current brief
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
@@ -509,8 +503,6 @@ export class ApiStorage implements IStorage {
|
|||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ensureBriefSelected('updateTaskStatus');
|
|
||||||
|
|
||||||
const existingTask = await this.retryOperation(() =>
|
const existingTask = await this.retryOperation(() =>
|
||||||
this.repository.getTask(this.projectId, taskId)
|
this.repository.getTask(this.projectId, taskId)
|
||||||
);
|
);
|
||||||
@@ -547,12 +539,12 @@ export class ApiStorage implements IStorage {
|
|||||||
taskId
|
taskId
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.wrapError(error, 'Failed to update task status via API', {
|
throw new TaskMasterError(
|
||||||
operation: 'updateTaskStatus',
|
'Failed to update task status via API',
|
||||||
taskId,
|
ERROR_CODES.STORAGE_ERROR,
|
||||||
newStatus,
|
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
|
||||||
tag
|
error as Error
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,29 +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
|
* Retry an operation with exponential backoff
|
||||||
*/
|
*/
|
||||||
@@ -811,28 +780,4 @@ export class ApiStorage implements IStorage {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrap an error unless it's already a NO_BRIEF_SELECTED error
|
|
||||||
*/
|
|
||||||
private wrapError(
|
|
||||||
error: unknown,
|
|
||||||
message: string,
|
|
||||||
context: Record<string, unknown>
|
|
||||||
): never {
|
|
||||||
// If it's already a NO_BRIEF_SELECTED error, don't wrap it
|
|
||||||
if (
|
|
||||||
error instanceof TaskMasterError &&
|
|
||||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
|
||||||
) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TaskMasterError(
|
|
||||||
message,
|
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
|
||||||
context,
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,13 +44,6 @@ export class FileStorage implements IStorage {
|
|||||||
await this.fileOps.cleanup();
|
await this.fileOps.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the storage type
|
|
||||||
*/
|
|
||||||
getType(): 'file' {
|
|
||||||
return 'file';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get statistics about the storage
|
* Get statistics about the storage
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export class StorageFactory {
|
|||||||
apiAccessToken: credentials.token,
|
apiAccessToken: credentials.token,
|
||||||
apiEndpoint:
|
apiEndpoint:
|
||||||
config.storage?.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;
|
config.storage = nextStorage;
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,6 @@ export class StorageFactory {
|
|||||||
apiAccessToken: credentials.token,
|
apiAccessToken: credentials.token,
|
||||||
apiEndpoint:
|
apiEndpoint:
|
||||||
config.storage?.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'
|
'https://tryhamster.com/api'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -201,44 +201,6 @@ export class TaskMasterCore {
|
|||||||
return this.taskService.getStorageType();
|
return this.taskService.getStorageType();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get storage configuration
|
|
||||||
*/
|
|
||||||
getStorageConfig() {
|
|
||||||
return this.configManager.getStorageConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get storage display information for headers
|
|
||||||
* Returns context info for API storage, null for file storage
|
|
||||||
*/
|
|
||||||
getStorageDisplayInfo(): {
|
|
||||||
briefId: string;
|
|
||||||
briefName: string;
|
|
||||||
orgSlug?: string;
|
|
||||||
} | null {
|
|
||||||
// Only return info if using API storage
|
|
||||||
const storageType = this.getStorageType();
|
|
||||||
if (storageType !== 'api') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get credentials from auth manager
|
|
||||||
const authManager = AuthManager.getInstance();
|
|
||||||
const credentials = authManager.getCredentials();
|
|
||||||
const selectedContext = credentials?.selectedContext;
|
|
||||||
|
|
||||||
if (!selectedContext?.briefId || !selectedContext?.briefName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
briefId: selectedContext.briefId,
|
|
||||||
briefName: selectedContext.briefName,
|
|
||||||
orgSlug: selectedContext.orgSlug
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current active tag
|
* Get current active tag
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
// Load .env BEFORE any other imports to ensure env vars are available
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Add at the very beginning of the file
|
// 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));
|
console.error('DEBUG - dev.js received args:', process.argv.slice(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use dynamic import to ensure dotenv.config() runs before module-level code executes
|
import { runCLI } from './modules/commands.js';
|
||||||
const { runCLI } = await import('./modules/commands.js');
|
|
||||||
|
|
||||||
// Run the CLI with the process arguments
|
// Run the CLI with the process arguments
|
||||||
runCLI(process.argv);
|
runCLI(process.argv);
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import {
|
|||||||
registerAllCommands,
|
registerAllCommands,
|
||||||
checkForUpdate,
|
checkForUpdate,
|
||||||
performAutoUpdate,
|
performAutoUpdate,
|
||||||
displayUpgradeNotification,
|
displayUpgradeNotification
|
||||||
displayError
|
|
||||||
} from '@tm/cli';
|
} from '@tm/cli';
|
||||||
|
|
||||||
import {
|
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
|
// add-dependency command
|
||||||
programInstance
|
programInstance
|
||||||
.command('add-dependency')
|
.command('add-dependency')
|
||||||
@@ -5157,7 +5207,10 @@ async function runCLI(argv = process.argv) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Generic error handling for other errors
|
// Generic error handling for other errors
|
||||||
displayError(error);
|
console.error(chalk.red(`Error: ${error.message}`));
|
||||||
|
if (getDebugFlag()) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user