Compare commits
10 Commits
ralph/fix/
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c5d5651a9 | ||
|
|
8649c8a347 | ||
|
|
f7cab246b0 | ||
|
|
5aca107827 | ||
|
|
fb68c9fe1f | ||
|
|
ff3bd7add8 | ||
|
|
c8228e913b | ||
|
|
218b68a31e | ||
|
|
6bc75c0ac6 | ||
|
|
d7fca1844f |
5
.changeset/metal-rocks-help.md
Normal file
5
.changeset/metal-rocks-help.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve next command to work with remote
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"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,6 +8,7 @@ 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';
|
||||||
@@ -45,6 +46,12 @@ 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,6 +14,7 @@ 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result type from auth command
|
* Result type from auth command
|
||||||
@@ -351,6 +352,37 @@ 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',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
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 {
|
import {
|
||||||
AuthManager,
|
AuthManager,
|
||||||
@@ -156,10 +157,14 @@ 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) {
|
if (context.briefName && context.briefId) {
|
||||||
|
const shortId = context.briefId.slice(0, 8);
|
||||||
|
console.log(
|
||||||
|
chalk.white(` ${context.briefName} `) + chalk.gray(`(${shortId})`)
|
||||||
|
);
|
||||||
|
} else if (context.briefName) {
|
||||||
console.log(chalk.white(` ${context.briefName}`));
|
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}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,25 +329,53 @@ export class ContextCommand extends Command {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for selection
|
// Prompt for selection with search
|
||||||
const { selectedBrief } = await inquirer.prompt([
|
const selectedBrief = await search<(typeof briefs)[0] | null>({
|
||||||
{
|
message: 'Search for a brief:',
|
||||||
type: 'list',
|
source: async (input) => {
|
||||||
name: 'selectedBrief',
|
const searchTerm = input?.toLowerCase() || '';
|
||||||
message: 'Select a brief:',
|
|
||||||
choices: [
|
// Static option for no brief
|
||||||
{ name: '(No brief - organization level)', value: null },
|
const noBriefOption = {
|
||||||
...briefs.map((brief) => ({
|
name: '(No brief - organization level)',
|
||||||
name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`,
|
value: null as any,
|
||||||
value: brief
|
description: 'Clear brief selection'
|
||||||
}))
|
};
|
||||||
]
|
|
||||||
|
// Filter and map brief options
|
||||||
|
const briefOptions = briefs
|
||||||
|
.filter((brief) => {
|
||||||
|
if (!searchTerm) return true;
|
||||||
|
|
||||||
|
const title = brief.document?.title || '';
|
||||||
|
const shortId = brief.id.slice(0, 8);
|
||||||
|
|
||||||
|
// Search by title first, then by UUID
|
||||||
|
return (
|
||||||
|
title.toLowerCase().includes(searchTerm) ||
|
||||||
|
brief.id.toLowerCase().includes(searchTerm) ||
|
||||||
|
shortId.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((brief) => {
|
||||||
|
const title =
|
||||||
|
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
|
||||||
|
const shortId = brief.id.slice(0, 8);
|
||||||
|
return {
|
||||||
|
name: `${title} ${chalk.gray(`(${shortId})`)}`,
|
||||||
|
value: brief
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [noBriefOption, ...briefOptions];
|
||||||
}
|
}
|
||||||
]);
|
});
|
||||||
|
|
||||||
if (selectedBrief) {
|
if (selectedBrief) {
|
||||||
// Update context with brief
|
// Update context with brief
|
||||||
const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`;
|
const briefName =
|
||||||
|
selectedBrief.document?.title ||
|
||||||
|
`Brief ${selectedBrief.id.slice(0, 8)}`;
|
||||||
this.authManager.updateContext({
|
this.authManager.updateContext({
|
||||||
briefId: selectedBrief.id,
|
briefId: selectedBrief.id,
|
||||||
briefName: briefName
|
briefName: briefName
|
||||||
@@ -354,7 +387,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.name}`
|
message: `Selected brief: ${selectedBrief.document?.title}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Clear brief selection
|
// Clear brief selection
|
||||||
@@ -468,7 +501,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_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
|
`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -490,7 +523,8 @@ export class ContextCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update context: set org and brief
|
// Update context: set org and brief
|
||||||
const briefName = `Brief ${brief.id.slice(0, 8)}`;
|
const briefName =
|
||||||
|
brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
|
||||||
this.authManager.updateContext({
|
this.authManager.updateContext({
|
||||||
orgId: brief.accountId,
|
orgId: brief.accountId,
|
||||||
orgName,
|
orgName,
|
||||||
@@ -686,6 +720,53 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
247
apps/cli/src/commands/next.command.ts
Normal file
247
apps/cli/src/commands/next.command.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
// 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';
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export function displayHeader(options: HeaderOptions = {}): void {
|
|||||||
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);
|
||||||
@@ -39,7 +39,5 @@ 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,45 @@ sidebarTitle: "CLI Commands"
|
|||||||
|
|
||||||
|
|
||||||
<AccordionGroup>
|
<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">
|
<Accordion title="Parse PRD">
|
||||||
```bash
|
```bash
|
||||||
# Parse a PRD file and generate tasks
|
# Parse a PRD file and generate tasks
|
||||||
|
|||||||
44
output.txt
Normal file
44
output.txt
Normal file
File diff suppressed because one or more lines are too long
86
package-lock.json
generated
86
package-lock.json
generated
@@ -104,6 +104,7 @@
|
|||||||
"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",
|
||||||
@@ -124,6 +125,91 @@
|
|||||||
"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,11 +7,13 @@ 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
|
||||||
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsup's env option
|
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
|
||||||
|
// Build-time: process.env.TM_PUBLIC_BASE_DOMAIN gets replaced by tsdown's env option
|
||||||
|
// Runtime: process.env.TM_BASE_DOMAIN can override for staging/development
|
||||||
// Default: https://tryhamster.com for production
|
// Default: https://tryhamster.com for production
|
||||||
const BASE_DOMAIN =
|
const BASE_DOMAIN =
|
||||||
process.env.TM_PUBLIC_BASE_DOMAIN || // This gets replaced at build time by tsup
|
process.env.TM_BASE_DOMAIN || // Runtime override (for staging/tux)
|
||||||
'https://tryhamster.com';
|
process.env.TM_PUBLIC_BASE_DOMAIN; // Build-time (baked into compiled code)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default authentication configuration
|
* Default authentication configuration
|
||||||
@@ -19,7 +21,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,6 +24,8 @@ 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);
|
||||||
@@ -84,7 +86,11 @@ export class CredentialStore {
|
|||||||
|
|
||||||
// Validate expiration time for tokens
|
// Validate expiration time for tokens
|
||||||
if (expiresAtMs === undefined) {
|
if (expiresAtMs === undefined) {
|
||||||
this.logger.warn('No valid expiration time provided for token');
|
// Only log this warning once to avoid spam during auth flows
|
||||||
|
if (!this.hasWarnedAboutMissingExpiration) {
|
||||||
|
this.logger.warn('No valid expiration time provided for token');
|
||||||
|
this.hasWarnedAboutMissingExpiration = true;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +180,9 @@ 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}`,
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ export class SupabaseAuthClient {
|
|||||||
*/
|
*/
|
||||||
getClient(): SupabaseJSClient {
|
getClient(): SupabaseJSClient {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
// Get Supabase configuration from environment - using TM_PUBLIC prefix
|
// Get Supabase configuration from environment
|
||||||
const supabaseUrl = process.env.TM_PUBLIC_SUPABASE_URL;
|
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
|
||||||
const supabaseAnonKey = process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
|
const supabaseUrl =
|
||||||
|
process.env.TM_SUPABASE_URL || process.env.TM_PUBLIC_SUPABASE_URL;
|
||||||
|
const supabaseAnonKey =
|
||||||
|
process.env.TM_SUPABASE_ANON_KEY ||
|
||||||
|
process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
|
||||||
if (!supabaseUrl || !supabaseAnonKey) {
|
if (!supabaseUrl || !supabaseAnonKey) {
|
||||||
throw new AuthenticationError(
|
throw new AuthenticationError(
|
||||||
'Supabase configuration missing. Please set TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY environment variables.',
|
'Supabase configuration missing. Please set TM_SUPABASE_URL and TM_SUPABASE_ANON_KEY (runtime) or TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY (build-time) environment variables.',
|
||||||
'CONFIG_MISSING'
|
'CONFIG_MISSING'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,11 +358,12 @@ 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 useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
|
const apiEndpoint =
|
||||||
|
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
|
||||||
|
|
||||||
if (useAPIEndpoint) {
|
if (apiEndpoint) {
|
||||||
// Use the new bulk import API endpoint
|
// Use the new bulk import API endpoint
|
||||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks`;
|
const apiUrl = `${apiEndpoint}/ai/api/v1/briefs/${briefId}/tasks`;
|
||||||
|
|
||||||
// Transform tasks to flat structure for API
|
// Transform tasks to flat structure for API
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ 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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,7 +177,12 @@ 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);
|
||||||
|
|
||||||
@@ -196,7 +207,14 @@ 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) {
|
||||||
@@ -224,7 +242,13 @@ 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();
|
||||||
@@ -253,7 +277,15 @@ 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) {
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ 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
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ 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_PUBLIC_BASE_DOMAIN ||
|
process.env.TM_BASE_DOMAIN ||
|
||||||
'https://tryhamster.com/api'
|
process.env.TM_PUBLIC_BASE_DOMAIN
|
||||||
};
|
};
|
||||||
config.storage = nextStorage;
|
config.storage = nextStorage;
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,7 @@ 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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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
|
||||||
@@ -16,7 +18,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
import { runCLI } from './modules/commands.js';
|
// Use dynamic import to ensure dotenv.config() runs before module-level code executes
|
||||||
|
const { runCLI } = await import('./modules/commands.js');
|
||||||
|
|
||||||
// Run the CLI with the process arguments
|
// Run the CLI with the process arguments
|
||||||
runCLI(process.argv);
|
runCLI(process.argv);
|
||||||
|
|||||||
@@ -2441,57 +2441,6 @@ ${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')
|
||||||
|
|||||||
Reference in New Issue
Block a user