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
21 changed files with 95 additions and 651 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,7 +14,6 @@ import {
type AuthCredentials
} from '@tm/core/auth';
import * as ui from '../utils/ui.js';
import { ContextCommand } from './context.command.js';
/**
* Result type from auth command
@@ -352,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',

View File

@@ -6,7 +6,6 @@
import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import search from '@inquirer/search';
import ora, { Ora } from 'ora';
import {
AuthManager,
@@ -157,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}`));
}
}
@@ -329,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
@@ -387,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
@@ -501,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);
}
@@ -523,8 +490,7 @@ export class ContextCommand extends Command {
}
// 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,
@@ -720,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

@@ -1,247 +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 { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayHeader } from '../ui/index.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> {
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) {
const msg = error?.getSanitizedDetails?.() ?? {
message: error?.message ?? String(error)
};
// Allow error to propagate for library compatibility
throw new Error(msg.message || 'Unexpected error in next command');
} finally {
// Always clean up resources, even on error
await this.cleanup();
}
}
/**
* 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 tag (no file path for next command)
displayHeader({
tag: result.tag || 'master'
});
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.gray('Storage: ' + result.storageType)}`);
console.log(
`\n${chalk.dim('Tip: Try')} ${chalk.cyan('task-master list --status pending')} ${chalk.dim('to see all pending tasks')}`
);
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
});
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
}
/**
* 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

@@ -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';

View File

@@ -25,9 +25,9 @@ export function displayHeader(options: HeaderOptions = {}): void {
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);
@@ -39,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

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

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

@@ -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

@@ -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

@@ -112,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

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

@@ -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

@@ -2441,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')