Compare commits
1 Commits
task-maste
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c4a25569 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix MCP connection errors caused by deprecated generateTaskFiles calls. Resolves "Cannot read properties of null (reading 'toString')" errors when using MCP tools for task management operations.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix MCP server error when file parameter not provided - now properly constructs default tasks.json path instead of failing with 'tasksJsonPath is required' error.
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Centralized Command Registry
|
|
||||||
* Provides a single location for registering all CLI commands
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
|
|
||||||
// Import all commands
|
|
||||||
import { ListTasksCommand } from './commands/list.command.js';
|
|
||||||
import { ShowCommand } from './commands/show.command.js';
|
|
||||||
import { AuthCommand } from './commands/auth.command.js';
|
|
||||||
import { ContextCommand } from './commands/context.command.js';
|
|
||||||
import { StartCommand } from './commands/start.command.js';
|
|
||||||
import { SetStatusCommand } from './commands/set-status.command.js';
|
|
||||||
import { ExportCommand } from './commands/export.command.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Command metadata for registration
|
|
||||||
*/
|
|
||||||
export interface CommandMetadata {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
commandClass: typeof Command;
|
|
||||||
category?: 'task' | 'auth' | 'utility' | 'development';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry of all available commands
|
|
||||||
*/
|
|
||||||
export class CommandRegistry {
|
|
||||||
/**
|
|
||||||
* All available commands with their metadata
|
|
||||||
*/
|
|
||||||
private static commands: CommandMetadata[] = [
|
|
||||||
// Task Management Commands
|
|
||||||
{
|
|
||||||
name: 'list',
|
|
||||||
description: 'List all tasks with filtering and status overview',
|
|
||||||
commandClass: ListTasksCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'show',
|
|
||||||
description: 'Display detailed information about a specific task',
|
|
||||||
commandClass: ShowCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'start',
|
|
||||||
description: 'Start working on a task with claude-code',
|
|
||||||
commandClass: StartCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'set-status',
|
|
||||||
description: 'Update the status of one or more tasks',
|
|
||||||
commandClass: SetStatusCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'export',
|
|
||||||
description: 'Export tasks to external systems',
|
|
||||||
commandClass: ExportCommand as any,
|
|
||||||
category: 'task'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Authentication & Context Commands
|
|
||||||
{
|
|
||||||
name: 'auth',
|
|
||||||
description: 'Manage authentication with tryhamster.com',
|
|
||||||
commandClass: AuthCommand as any,
|
|
||||||
category: 'auth'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'context',
|
|
||||||
description: 'Manage workspace context (organization/brief)',
|
|
||||||
commandClass: ContextCommand as any,
|
|
||||||
category: 'auth'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register all commands on a program instance
|
|
||||||
* @param program - Commander program to register commands on
|
|
||||||
*/
|
|
||||||
static registerAll(program: Command): void {
|
|
||||||
for (const cmd of this.commands) {
|
|
||||||
this.registerCommand(program, cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register specific commands by category
|
|
||||||
* @param program - Commander program to register commands on
|
|
||||||
* @param category - Category of commands to register
|
|
||||||
*/
|
|
||||||
static registerByCategory(
|
|
||||||
program: Command,
|
|
||||||
category: 'task' | 'auth' | 'utility' | 'development'
|
|
||||||
): void {
|
|
||||||
const categoryCommands = this.commands.filter(
|
|
||||||
(cmd) => cmd.category === category
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const cmd of categoryCommands) {
|
|
||||||
this.registerCommand(program, cmd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a single command by name
|
|
||||||
* @param program - Commander program to register the command on
|
|
||||||
* @param name - Name of the command to register
|
|
||||||
*/
|
|
||||||
static registerByName(program: Command, name: string): void {
|
|
||||||
const cmd = this.commands.find((c) => c.name === name);
|
|
||||||
if (cmd) {
|
|
||||||
this.registerCommand(program, cmd);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Command '${name}' not found in registry`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a single command
|
|
||||||
* @param program - Commander program to register the command on
|
|
||||||
* @param metadata - Command metadata
|
|
||||||
*/
|
|
||||||
private static registerCommand(
|
|
||||||
program: Command,
|
|
||||||
metadata: CommandMetadata
|
|
||||||
): void {
|
|
||||||
const CommandClass = metadata.commandClass as any;
|
|
||||||
|
|
||||||
// Use the static registration method that all commands have
|
|
||||||
if (CommandClass.registerOn) {
|
|
||||||
CommandClass.registerOn(program);
|
|
||||||
} else if (CommandClass.register) {
|
|
||||||
CommandClass.register(program);
|
|
||||||
} else {
|
|
||||||
// Fallback to creating instance and adding
|
|
||||||
const instance = new CommandClass();
|
|
||||||
program.addCommand(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all registered command names
|
|
||||||
*/
|
|
||||||
static getCommandNames(): string[] {
|
|
||||||
return this.commands.map((cmd) => cmd.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get commands by category
|
|
||||||
*/
|
|
||||||
static getCommandsByCategory(
|
|
||||||
category: 'task' | 'auth' | 'utility' | 'development'
|
|
||||||
): CommandMetadata[] {
|
|
||||||
return this.commands.filter((cmd) => cmd.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new command to the registry
|
|
||||||
* @param metadata - Command metadata to add
|
|
||||||
*/
|
|
||||||
static addCommand(metadata: CommandMetadata): void {
|
|
||||||
// Check if command already exists
|
|
||||||
if (this.commands.some((cmd) => cmd.name === metadata.name)) {
|
|
||||||
throw new Error(`Command '${metadata.name}' already exists in registry`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.commands.push(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a command from the registry
|
|
||||||
* @param name - Name of the command to remove
|
|
||||||
*/
|
|
||||||
static removeCommand(name: string): boolean {
|
|
||||||
const index = this.commands.findIndex((cmd) => cmd.name === name);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.commands.splice(index, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get command metadata by name
|
|
||||||
* @param name - Name of the command
|
|
||||||
*/
|
|
||||||
static getCommand(name: string): CommandMetadata | undefined {
|
|
||||||
return this.commands.find((cmd) => cmd.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a command exists
|
|
||||||
* @param name - Name of the command
|
|
||||||
*/
|
|
||||||
static hasCommand(name: string): boolean {
|
|
||||||
return this.commands.some((cmd) => cmd.name === name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a formatted list of all commands for display
|
|
||||||
*/
|
|
||||||
static getFormattedCommandList(): string {
|
|
||||||
const categories = {
|
|
||||||
task: 'Task Management',
|
|
||||||
auth: 'Authentication & Context',
|
|
||||||
utility: 'Utilities',
|
|
||||||
development: 'Development'
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (const [category, title] of Object.entries(categories)) {
|
|
||||||
const cmds = this.getCommandsByCategory(
|
|
||||||
category as keyof typeof categories
|
|
||||||
);
|
|
||||||
if (cmds.length > 0) {
|
|
||||||
output += `\n${title}:\n`;
|
|
||||||
for (const cmd of cmds) {
|
|
||||||
output += ` ${cmd.name.padEnd(20)} ${cmd.description}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to register all CLI commands
|
|
||||||
* @param program - Commander program instance
|
|
||||||
*/
|
|
||||||
export function registerAllCommands(program: Command): void {
|
|
||||||
CommandRegistry.registerAll(program);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to register commands by category
|
|
||||||
* @param program - Commander program instance
|
|
||||||
* @param category - Category to register
|
|
||||||
*/
|
|
||||||
export function registerCommandsByCategory(
|
|
||||||
program: Command,
|
|
||||||
category: 'task' | 'auth' | 'utility' | 'development'
|
|
||||||
): void {
|
|
||||||
CommandRegistry.registerByCategory(program, category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the registry for direct access if needed
|
|
||||||
export default CommandRegistry;
|
|
||||||
@@ -493,7 +493,18 @@ export class AuthCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
* This is for gradual migration - allows commands.js to use this
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const authCommand = new AuthCommand();
|
||||||
|
program.addCommand(authCommand);
|
||||||
|
return authCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
|
* Can also configure the command name if needed
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): AuthCommand {
|
static register(program: Command, name?: string): AuthCommand {
|
||||||
const authCommand = new AuthCommand(name);
|
const authCommand = new AuthCommand(name);
|
||||||
|
|||||||
@@ -694,7 +694,16 @@ export class ContextCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const contextCommand = new ContextCommand();
|
||||||
|
program.addCommand(contextCommand);
|
||||||
|
return contextCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): ContextCommand {
|
static register(program: Command, name?: string): ContextCommand {
|
||||||
const contextCommand = new ContextCommand(name);
|
const contextCommand = new ContextCommand(name);
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Export command for exporting tasks to external systems
|
|
||||||
* Provides functionality to export tasks to Hamster briefs
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import inquirer from 'inquirer';
|
|
||||||
import ora, { Ora } from 'ora';
|
|
||||||
import {
|
|
||||||
AuthManager,
|
|
||||||
AuthenticationError,
|
|
||||||
type UserContext
|
|
||||||
} from '@tm/core/auth';
|
|
||||||
import { TaskMasterCore, type ExportResult } from '@tm/core';
|
|
||||||
import * as ui from '../utils/ui.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result type from export command
|
|
||||||
*/
|
|
||||||
export interface ExportCommandResult {
|
|
||||||
success: boolean;
|
|
||||||
action: 'export' | 'validate' | 'cancelled';
|
|
||||||
result?: ExportResult;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExportCommand extending Commander's Command class
|
|
||||||
* Handles task export to external systems
|
|
||||||
*/
|
|
||||||
export class ExportCommand extends Command {
|
|
||||||
private authManager: AuthManager;
|
|
||||||
private taskMasterCore?: TaskMasterCore;
|
|
||||||
private lastResult?: ExportCommandResult;
|
|
||||||
|
|
||||||
constructor(name?: string) {
|
|
||||||
super(name || 'export');
|
|
||||||
|
|
||||||
// Initialize auth manager
|
|
||||||
this.authManager = AuthManager.getInstance();
|
|
||||||
|
|
||||||
// Configure the command
|
|
||||||
this.description('Export tasks to external systems (e.g., Hamster briefs)');
|
|
||||||
|
|
||||||
// Add options
|
|
||||||
this.option('--org <id>', 'Organization ID to export to');
|
|
||||||
this.option('--brief <id>', 'Brief ID to export tasks to');
|
|
||||||
this.option('--tag <tag>', 'Export tasks from a specific tag');
|
|
||||||
this.option(
|
|
||||||
'--status <status>',
|
|
||||||
'Filter tasks by status (pending, in-progress, done, etc.)'
|
|
||||||
);
|
|
||||||
this.option('--exclude-subtasks', 'Exclude subtasks from export');
|
|
||||||
this.option('-y, --yes', 'Skip confirmation prompt');
|
|
||||||
|
|
||||||
// Accept optional positional argument for brief ID or Hamster URL
|
|
||||||
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
|
|
||||||
|
|
||||||
// Default action
|
|
||||||
this.action(async (briefOrUrl?: string, options?: any) => {
|
|
||||||
await this.executeExport(briefOrUrl, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the TaskMasterCore
|
|
||||||
*/
|
|
||||||
private async initializeServices(): Promise<void> {
|
|
||||||
if (this.taskMasterCore) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize TaskMasterCore
|
|
||||||
this.taskMasterCore = await TaskMasterCore.create({
|
|
||||||
projectPath: process.cwd()
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to initialize services: ${(error as Error).message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the export command
|
|
||||||
*/
|
|
||||||
private async executeExport(
|
|
||||||
briefOrUrl?: string,
|
|
||||||
options?: any
|
|
||||||
): Promise<void> {
|
|
||||||
let spinner: Ora | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
ui.displayError('Not authenticated. Run "tm auth login" first.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize services
|
|
||||||
await this.initializeServices();
|
|
||||||
|
|
||||||
// Get current context
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
|
|
||||||
// Determine org and brief IDs
|
|
||||||
let orgId = options?.org || context?.orgId;
|
|
||||||
let briefId = options?.brief || briefOrUrl || context?.briefId;
|
|
||||||
|
|
||||||
// If a URL/ID was provided as argument, resolve it
|
|
||||||
if (briefOrUrl && !options?.brief) {
|
|
||||||
spinner = ora('Resolving brief...').start();
|
|
||||||
const resolvedBrief = await this.resolveBriefInput(briefOrUrl);
|
|
||||||
if (resolvedBrief) {
|
|
||||||
briefId = resolvedBrief.briefId;
|
|
||||||
orgId = resolvedBrief.orgId;
|
|
||||||
spinner.succeed('Brief resolved');
|
|
||||||
} else {
|
|
||||||
spinner.fail('Could not resolve brief');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate we have necessary IDs
|
|
||||||
if (!orgId) {
|
|
||||||
ui.displayError(
|
|
||||||
'No organization selected. Run "tm context org" or use --org flag.'
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!briefId) {
|
|
||||||
ui.displayError(
|
|
||||||
'No brief specified. Run "tm context brief", provide a brief ID/URL, or use --brief flag.'
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm export if not auto-confirmed
|
|
||||||
if (!options?.yes) {
|
|
||||||
const confirmed = await this.confirmExport(orgId, briefId, context);
|
|
||||||
if (!confirmed) {
|
|
||||||
ui.displayWarning('Export cancelled');
|
|
||||||
this.lastResult = {
|
|
||||||
success: false,
|
|
||||||
action: 'cancelled',
|
|
||||||
message: 'User cancelled export'
|
|
||||||
};
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform export
|
|
||||||
spinner = ora('Exporting tasks...').start();
|
|
||||||
|
|
||||||
const exportResult = await this.taskMasterCore!.exportTasks({
|
|
||||||
orgId,
|
|
||||||
briefId,
|
|
||||||
tag: options?.tag,
|
|
||||||
status: options?.status,
|
|
||||||
excludeSubtasks: options?.excludeSubtasks || false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exportResult.success) {
|
|
||||||
spinner.succeed(
|
|
||||||
`Successfully exported ${exportResult.taskCount} task(s) to brief`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Display summary
|
|
||||||
console.log(chalk.cyan('\n📤 Export Summary\n'));
|
|
||||||
console.log(chalk.white(` Organization: ${orgId}`));
|
|
||||||
console.log(chalk.white(` Brief: ${briefId}`));
|
|
||||||
console.log(chalk.white(` Tasks exported: ${exportResult.taskCount}`));
|
|
||||||
if (options?.tag) {
|
|
||||||
console.log(chalk.gray(` Tag: ${options.tag}`));
|
|
||||||
}
|
|
||||||
if (options?.status) {
|
|
||||||
console.log(chalk.gray(` Status filter: ${options.status}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exportResult.message) {
|
|
||||||
console.log(chalk.gray(`\n ${exportResult.message}`));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
spinner.fail('Export failed');
|
|
||||||
if (exportResult.error) {
|
|
||||||
console.error(chalk.red(`\n✗ ${exportResult.error.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastResult = {
|
|
||||||
success: exportResult.success,
|
|
||||||
action: 'export',
|
|
||||||
result: exportResult
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
if (spinner?.isSpinning) spinner.fail('Export failed');
|
|
||||||
this.handleError(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve brief input to get brief and org IDs
|
|
||||||
*/
|
|
||||||
private async resolveBriefInput(
|
|
||||||
briefOrUrl: string
|
|
||||||
): Promise<{ briefId: string; orgId: string } | null> {
|
|
||||||
try {
|
|
||||||
// Extract brief ID from input
|
|
||||||
const briefId = this.extractBriefId(briefOrUrl);
|
|
||||||
if (!briefId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch brief to get organization
|
|
||||||
const brief = await this.authManager.getBrief(briefId);
|
|
||||||
if (!brief) {
|
|
||||||
ui.displayError('Brief not found or you do not have access');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
briefId: brief.id,
|
|
||||||
orgId: brief.accountId
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(chalk.red(`Failed to resolve brief: ${error}`));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a brief ID from raw input (ID or URL)
|
|
||||||
*/
|
|
||||||
private extractBriefId(input: string): string | null {
|
|
||||||
const raw = input?.trim() ?? '';
|
|
||||||
if (!raw) return null;
|
|
||||||
|
|
||||||
const parseUrl = (s: string): URL | null => {
|
|
||||||
try {
|
|
||||||
return new URL(s);
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
return new URL(`https://${s}`);
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fromParts = (path: string): string | null => {
|
|
||||||
const parts = path.split('/').filter(Boolean);
|
|
||||||
const briefsIdx = parts.lastIndexOf('briefs');
|
|
||||||
const candidate =
|
|
||||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
|
||||||
? parts[briefsIdx + 1]
|
|
||||||
: parts[parts.length - 1];
|
|
||||||
return candidate?.trim() || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try URL parsing
|
|
||||||
const url = parseUrl(raw);
|
|
||||||
if (url) {
|
|
||||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
|
||||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
|
||||||
if (candidate) {
|
|
||||||
if (this.isLikelyId(candidate) || candidate.length >= 8) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it looks like a path
|
|
||||||
if (raw.includes('/')) {
|
|
||||||
const candidate = fromParts(raw);
|
|
||||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return raw if it looks like an ID
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string looks like a brief ID
|
|
||||||
*/
|
|
||||||
private isLikelyId(value: string): boolean {
|
|
||||||
const uuidRegex =
|
|
||||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
||||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
||||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
|
|
||||||
return (
|
|
||||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm export with the user
|
|
||||||
*/
|
|
||||||
private async confirmExport(
|
|
||||||
orgId: string,
|
|
||||||
briefId: string,
|
|
||||||
context: UserContext | null
|
|
||||||
): Promise<boolean> {
|
|
||||||
console.log(chalk.cyan('\n📤 Export Tasks\n'));
|
|
||||||
|
|
||||||
// Show org name if available
|
|
||||||
if (context?.orgName) {
|
|
||||||
console.log(chalk.white(` Organization: ${context.orgName}`));
|
|
||||||
console.log(chalk.gray(` ID: ${orgId}`));
|
|
||||||
} else {
|
|
||||||
console.log(chalk.white(` Organization ID: ${orgId}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show brief info
|
|
||||||
if (context?.briefName) {
|
|
||||||
console.log(chalk.white(`\n Brief: ${context.briefName}`));
|
|
||||||
console.log(chalk.gray(` ID: ${briefId}`));
|
|
||||||
} else {
|
|
||||||
console.log(chalk.white(`\n Brief ID: ${briefId}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { confirmed } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'confirmed',
|
|
||||||
message: 'Do you want to proceed with export?',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
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)
|
|
||||||
*/
|
|
||||||
public getLastResult(): ExportCommandResult | undefined {
|
|
||||||
return this.lastResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up resources
|
|
||||||
*/
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
// No resources to clean up
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register this command on an existing program
|
|
||||||
*/
|
|
||||||
static register(program: Command, name?: string): ExportCommand {
|
|
||||||
const exportCommand = new ExportCommand(name);
|
|
||||||
program.addCommand(exportCommand);
|
|
||||||
return exportCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -246,7 +246,7 @@ export class ListTasksCommand extends Command {
|
|||||||
task.subtasks.forEach((subtask) => {
|
task.subtasks.forEach((subtask) => {
|
||||||
const subIcon = STATUS_ICONS[subtask.status];
|
const subIcon = STATUS_ICONS[subtask.status];
|
||||||
console.log(
|
console.log(
|
||||||
` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
|
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ export class ListTasksCommand extends Command {
|
|||||||
nextTask
|
nextTask
|
||||||
);
|
);
|
||||||
|
|
||||||
// Task table
|
// Task table - no title, just show the table directly
|
||||||
console.log(
|
console.log(
|
||||||
ui.createTaskTable(tasks, {
|
ui.createTaskTable(tasks, {
|
||||||
showSubtasks: withSubtasks,
|
showSubtasks: withSubtasks,
|
||||||
@@ -474,7 +474,18 @@ export class ListTasksCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
* This is for gradual migration - allows commands.js to use this
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const listCommand = new ListTasksCommand();
|
||||||
|
program.addCommand(listCommand);
|
||||||
|
return listCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
|
* Can also configure the command name if needed
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): ListTasksCommand {
|
static register(program: Command, name?: string): ListTasksCommand {
|
||||||
const listCommand = new ListTasksCommand(name);
|
const listCommand = new ListTasksCommand(name);
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ export class SetStatusCommand extends Command {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show storage info
|
||||||
|
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,7 +290,18 @@ export class SetStatusCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
* This is for gradual migration - allows commands.js to use this
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const setStatusCommand = new SetStatusCommand();
|
||||||
|
program.addCommand(setStatusCommand);
|
||||||
|
return setStatusCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
|
* Can also configure the command name if needed
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): SetStatusCommand {
|
static register(program: Command, name?: string): SetStatusCommand {
|
||||||
const setStatusCommand = new SetStatusCommand(name);
|
const setStatusCommand = new SetStatusCommand(name);
|
||||||
|
|||||||
@@ -322,7 +322,18 @@ export class ShowCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
* This is for gradual migration - allows commands.js to use this
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const showCommand = new ShowCommand();
|
||||||
|
program.addCommand(showCommand);
|
||||||
|
return showCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
|
* Can also configure the command name if needed
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): ShowCommand {
|
static register(program: Command, name?: string): ShowCommand {
|
||||||
const showCommand = new ShowCommand(name);
|
const showCommand = new ShowCommand(name);
|
||||||
|
|||||||
@@ -493,7 +493,16 @@ export class StartCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this command on an existing program
|
* Static method to register this command on an existing program
|
||||||
|
*/
|
||||||
|
static registerOn(program: Command): Command {
|
||||||
|
const startCommand = new StartCommand();
|
||||||
|
program.addCommand(startCommand);
|
||||||
|
return startCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternative registration that returns the command for chaining
|
||||||
*/
|
*/
|
||||||
static register(program: Command, name?: string): StartCommand {
|
static register(program: Command, name?: string): StartCommand {
|
||||||
const startCommand = new StartCommand(name);
|
const startCommand = new StartCommand(name);
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ 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';
|
||||||
export { SetStatusCommand } from './commands/set-status.command.js';
|
export { SetStatusCommand } from './commands/set-status.command.js';
|
||||||
export { ExportCommand } from './commands/export.command.js';
|
|
||||||
|
|
||||||
// Command Registry
|
|
||||||
export {
|
|
||||||
CommandRegistry,
|
|
||||||
registerAllCommands,
|
|
||||||
registerCommandsByCategory,
|
|
||||||
type CommandMetadata
|
|
||||||
} from './command-registry.js';
|
|
||||||
|
|
||||||
// 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';
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ export function displaySubtasks(
|
|||||||
status: any;
|
status: any;
|
||||||
description?: string;
|
description?: string;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
}>
|
}>,
|
||||||
|
parentId: string | number
|
||||||
): void {
|
): void {
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||||
// Display subtasks header
|
// Display subtasks header
|
||||||
@@ -227,7 +228,7 @@ export function displaySubtasks(
|
|||||||
});
|
});
|
||||||
|
|
||||||
subtasks.forEach((subtask) => {
|
subtasks.forEach((subtask) => {
|
||||||
const subtaskId = String(subtask.id);
|
const subtaskId = `${parentId}.${subtask.id}`;
|
||||||
|
|
||||||
// Format dependencies
|
// Format dependencies
|
||||||
const deps =
|
const deps =
|
||||||
@@ -328,7 +329,7 @@ export function displayTaskDetails(
|
|||||||
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
||||||
} else if (filteredSubtasks.length > 0) {
|
} else if (filteredSubtasks.length > 0) {
|
||||||
console.log(); // Empty line for spacing
|
console.log(); // Empty line for spacing
|
||||||
displaySubtasks(filteredSubtasks);
|
displaySubtasks(filteredSubtasks, task.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,12 +286,12 @@ export function createTaskTable(
|
|||||||
// Adjust column widths to better match the original layout
|
// Adjust column widths to better match the original layout
|
||||||
const baseColWidths = showComplexity
|
const baseColWidths = showComplexity
|
||||||
? [
|
? [
|
||||||
Math.floor(terminalWidth * 0.1),
|
Math.floor(terminalWidth * 0.06),
|
||||||
Math.floor(terminalWidth * 0.4),
|
Math.floor(terminalWidth * 0.4),
|
||||||
Math.floor(terminalWidth * 0.15),
|
Math.floor(terminalWidth * 0.15),
|
||||||
Math.floor(terminalWidth * 0.1),
|
Math.floor(terminalWidth * 0.12),
|
||||||
Math.floor(terminalWidth * 0.2),
|
Math.floor(terminalWidth * 0.2),
|
||||||
Math.floor(terminalWidth * 0.1)
|
Math.floor(terminalWidth * 0.12)
|
||||||
] // ID, Title, Status, Priority, Dependencies, Complexity
|
] // ID, Title, Status, Priority, Dependencies, Complexity
|
||||||
: [
|
: [
|
||||||
Math.floor(terminalWidth * 0.08),
|
Math.floor(terminalWidth * 0.08),
|
||||||
@@ -377,11 +377,7 @@ export function createTaskTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showComplexity) {
|
if (showComplexity) {
|
||||||
const complexityDisplay =
|
subRow.push(chalk.gray('--'));
|
||||||
typeof subtask.complexity === 'number'
|
|
||||||
? getComplexityWithColor(subtask.complexity)
|
|
||||||
: '--';
|
|
||||||
subRow.push(chalk.gray(complexityDisplay));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.push(subRow);
|
table.push(subRow);
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ Taskmaster uses two primary methods for configuration:
|
|||||||
- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
|
- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
|
||||||
- `OPENROUTER_API_KEY`: Your OpenRouter API key.
|
- `OPENROUTER_API_KEY`: Your OpenRouter API key.
|
||||||
- `XAI_API_KEY`: Your X-AI API key.
|
- `XAI_API_KEY`: Your X-AI API key.
|
||||||
|
- `OPENAI_CODEX_API_KEY`: Your OpenAI API key for Codex CLI (optional - OAuth preferred).
|
||||||
- **Optional Endpoint Overrides:**
|
- **Optional Endpoint Overrides:**
|
||||||
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
|
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
|
||||||
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
|
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
|
||||||
@@ -316,4 +317,89 @@ Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure c
|
|||||||
- Confirm the model is deployed in your Azure OpenAI resource
|
- Confirm the model is deployed in your Azure OpenAI resource
|
||||||
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
- Verify the deployment name matches your configuration exactly (case-sensitive)
|
||||||
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
|
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
|
||||||
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
|
||||||
|
|
||||||
|
### Codex CLI Configuration
|
||||||
|
|
||||||
|
The Codex CLI provider integrates with OpenAI's ChatGPT subscription through the Codex CLI tool, providing access to advanced models like GPT-5 and GPT-5-Codex.
|
||||||
|
|
||||||
|
1. **Prerequisites**:
|
||||||
|
- Node.js >= 18.0.0
|
||||||
|
- Codex CLI >= 0.42.0 (>= 0.44.0 recommended)
|
||||||
|
- Active ChatGPT subscription (Plus, Pro, Business, Edu, or Enterprise)
|
||||||
|
|
||||||
|
2. **Installation**:
|
||||||
|
```bash
|
||||||
|
# Install Codex CLI globally
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# Authenticate with your ChatGPT account
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configuration Options**:
|
||||||
|
|
||||||
|
**Basic Configuration**
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advanced Configuration with Codex CLI Settings**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write",
|
||||||
|
"verbose": false,
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Authentication**:
|
||||||
|
- **Primary Method**: OAuth authentication via `codex login` (recommended)
|
||||||
|
- **Optional**: Set `OPENAI_CODEX_API_KEY` environment variable as fallback
|
||||||
|
- Note: API key doesn't provide access to subscription-only models like GPT-5-Codex
|
||||||
|
|
||||||
|
5. **Available Settings**:
|
||||||
|
- **`allowNpx`** (boolean): Allow fallback to `npx @openai/codex` if CLI not found
|
||||||
|
- **`skipGitRepoCheck`** (boolean): Skip git repository safety check
|
||||||
|
- **`approvalMode`** (string): Control command approval (`"untrusted"`, `"on-failure"`, `"on-request"`, `"never"`)
|
||||||
|
- **`sandboxMode`** (string): Control filesystem access (`"read-only"`, `"workspace-write"`, `"danger-full-access"`)
|
||||||
|
- **`codexPath`** (string): Custom path to Codex CLI executable
|
||||||
|
- **`cwd`** (string): Working directory for execution
|
||||||
|
- **`verbose`** (boolean): Enable verbose logging
|
||||||
|
- **`commandSpecific`** (object): Override settings for specific commands
|
||||||
|
|
||||||
|
6. **Security Notes**:
|
||||||
|
- Default settings prioritize safety with approval and sandbox modes
|
||||||
|
- Use `fullAuto: true` or `dangerouslyBypassApprovalsAndSandbox: true` with extreme caution
|
||||||
|
- Codebase analysis is automatically enabled for this provider
|
||||||
@@ -52,6 +52,18 @@
|
|||||||
"capabilities/cli-root-commands",
|
"capabilities/cli-root-commands",
|
||||||
"capabilities/task-structure"
|
"capabilities/task-structure"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "AI Providers",
|
||||||
|
"pages": [
|
||||||
|
"providers/codex-cli"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Usage Examples",
|
||||||
|
"pages": [
|
||||||
|
"examples/codex-cli-usage"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
466
apps/docs/examples/codex-cli-usage.mdx
Normal file
466
apps/docs/examples/codex-cli-usage.mdx
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
---
|
||||||
|
title: Codex CLI Provider Usage Examples
|
||||||
|
sidebarTitle: "Codex CLI Usage"
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide provides practical examples of using Task Master with the Codex CLI provider.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using these examples, ensure you have:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Codex CLI installed
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# 2. Authenticated with ChatGPT
|
||||||
|
codex login
|
||||||
|
|
||||||
|
# 3. Codex CLI configured as your provider
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 1: Basic Task Creation
|
||||||
|
|
||||||
|
Use Codex CLI to create tasks from a simple description:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a task with AI-powered enhancement
|
||||||
|
task-master add-task --prompt="Implement user authentication with JWT" --research
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Task Master sends your prompt to GPT-5-Codex via the CLI
|
||||||
|
2. The AI analyzes your request and generates a detailed task
|
||||||
|
3. The task is added to your `.taskmaster/tasks/tasks.json`
|
||||||
|
4. OAuth credentials are automatically used (no API key needed)
|
||||||
|
|
||||||
|
## Example 2: Parsing a Product Requirements Document
|
||||||
|
|
||||||
|
Create a comprehensive task list from a PRD:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create your PRD
|
||||||
|
cat > my-feature.txt <<EOF
|
||||||
|
# User Profile Feature
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
1. Users can view their profile
|
||||||
|
2. Users can edit their information
|
||||||
|
3. Profile pictures can be uploaded
|
||||||
|
4. Email verification required
|
||||||
|
|
||||||
|
## Technical Constraints
|
||||||
|
- Use React for frontend
|
||||||
|
- Node.js/Express backend
|
||||||
|
- PostgreSQL database
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Parse with Codex CLI
|
||||||
|
task-master parse-prd my-feature.txt --num-tasks 12
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. GPT-5-Codex reads and analyzes your PRD
|
||||||
|
2. Generates structured tasks with dependencies
|
||||||
|
3. Creates subtasks for complex items
|
||||||
|
4. Saves everything to `.taskmaster/tasks/`
|
||||||
|
|
||||||
|
## Example 3: Expanding Tasks with Research
|
||||||
|
|
||||||
|
Break down a complex task into detailed subtasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First, show your current tasks
|
||||||
|
task-master list
|
||||||
|
|
||||||
|
# Expand a specific task (e.g., task 1.2)
|
||||||
|
task-master expand --id=1.2 --research --force
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Codex CLI uses GPT-5 for research-level analysis
|
||||||
|
2. Breaks down the task into logical subtasks
|
||||||
|
3. Adds implementation details and test strategies
|
||||||
|
4. Updates the task with dependency information
|
||||||
|
|
||||||
|
## Example 4: Analyzing Project Complexity
|
||||||
|
|
||||||
|
Get AI-powered insights into your project's task complexity:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Analyze all tasks
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# View the complexity report
|
||||||
|
task-master complexity-report
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. GPT-5 analyzes each task's scope and requirements
|
||||||
|
2. Assigns complexity scores and estimates subtask counts
|
||||||
|
3. Generates a detailed report
|
||||||
|
4. Saves to `.taskmaster/reports/task-complexity-report.json`
|
||||||
|
|
||||||
|
## Example 5: Using Custom Codex CLI Settings
|
||||||
|
|
||||||
|
Configure Codex CLI behavior for different commands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write",
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"verbose": true,
|
||||||
|
"approvalMode": "never"
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only",
|
||||||
|
"verbose": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Now parse-prd runs with verbose output and no approvals
|
||||||
|
task-master parse-prd requirements.txt
|
||||||
|
|
||||||
|
# Expand runs with read-only mode
|
||||||
|
task-master expand --id=2.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 6: Workflow - Building a Feature End-to-End
|
||||||
|
|
||||||
|
Complete workflow from PRD to implementation tracking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: Initialize project
|
||||||
|
task-master init
|
||||||
|
|
||||||
|
# Step 2: Set up Codex CLI
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Step 3: Create PRD
|
||||||
|
cat > feature-prd.txt <<EOF
|
||||||
|
# Authentication System
|
||||||
|
|
||||||
|
Implement a complete authentication system with:
|
||||||
|
- User registration
|
||||||
|
- Email verification
|
||||||
|
- Password reset
|
||||||
|
- Two-factor authentication
|
||||||
|
- Session management
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Step 4: Parse PRD into tasks
|
||||||
|
task-master parse-prd feature-prd.txt --num-tasks 8
|
||||||
|
|
||||||
|
# Step 5: Analyze complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Step 6: Expand complex tasks
|
||||||
|
task-master expand --all --research
|
||||||
|
|
||||||
|
# Step 7: Start working
|
||||||
|
task-master next
|
||||||
|
# Shows: Task 1.1: User registration database schema
|
||||||
|
|
||||||
|
# Step 8: Mark completed as you work
|
||||||
|
task-master set-status --id=1.1 --status=done
|
||||||
|
|
||||||
|
# Step 9: Continue to next task
|
||||||
|
task-master next
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 7: Multi-Role Configuration
|
||||||
|
|
||||||
|
Use Codex CLI for main tasks, Perplexity for research:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// In .taskmaster/config.json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"research": {
|
||||||
|
"provider": "perplexity",
|
||||||
|
"modelId": "sonar-pro",
|
||||||
|
"maxTokens": 8700,
|
||||||
|
"temperature": 0.1
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Main task operations use GPT-5-Codex
|
||||||
|
task-master add-task --prompt="Build REST API endpoint"
|
||||||
|
|
||||||
|
# Research operations use Perplexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Fallback to GPT-5 if needed
|
||||||
|
task-master expand --id=3.2 --force
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 8: Troubleshooting Common Issues
|
||||||
|
|
||||||
|
### Issue: Codex CLI not found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Codex is installed
|
||||||
|
codex --version
|
||||||
|
|
||||||
|
# If not found, install globally
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# Or enable npx fallback in config
|
||||||
|
cat >> .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Not authenticated
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check auth status
|
||||||
|
codex
|
||||||
|
# Use /about command to see auth info
|
||||||
|
|
||||||
|
# Re-authenticate if needed
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Want more verbose output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable verbose mode in config
|
||||||
|
cat >> .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"verbose": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Or for specific commands
|
||||||
|
task-master parse-prd my-prd.txt
|
||||||
|
# (verbose output shows detailed Codex CLI interactions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 9: CI/CD Integration
|
||||||
|
|
||||||
|
Use Codex CLI in automated workflows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/task-analysis.yml
|
||||||
|
name: Analyze Task Complexity
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.taskmaster/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Task Master
|
||||||
|
run: npm install -g task-master-ai
|
||||||
|
|
||||||
|
- name: Configure Codex CLI
|
||||||
|
run: |
|
||||||
|
npm install -g @openai/codex
|
||||||
|
echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
|
||||||
|
env:
|
||||||
|
OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
|
||||||
|
|
||||||
|
- name: Configure Task Master
|
||||||
|
run: |
|
||||||
|
cat > .taskmaster/config.json <<EOF
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "never",
|
||||||
|
"fullAuto": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Analyze Complexity
|
||||||
|
run: task-master analyze-complexity --research
|
||||||
|
|
||||||
|
- name: Upload Report
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: complexity-report
|
||||||
|
path: .taskmaster/reports/task-complexity-report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use OAuth for Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For local development, use OAuth (no API key needed)
|
||||||
|
codex login
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Approval Modes Appropriately
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"approvalMode": "on-failure", // Safe default
|
||||||
|
"sandboxMode": "workspace-write" // Restricts to project directory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use Command-Specific Settings
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never", // PRD parsing is safe
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"approvalMode": "on-request", // More cautious for task expansion
|
||||||
|
"verbose": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Leverage Codebase Analysis
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"enableCodebaseAnalysis": true // Let Codex analyze your code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Handle Errors Gracefully
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always configure a fallback model
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Or use a different provider as fallback
|
||||||
|
task-master models --set-fallback claude-3-5-sonnet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pattern: Daily Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Morning: Review tasks
|
||||||
|
task-master list
|
||||||
|
|
||||||
|
# Get next task
|
||||||
|
task-master next
|
||||||
|
|
||||||
|
# Work on task...
|
||||||
|
|
||||||
|
# Update task with notes
|
||||||
|
task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
|
||||||
|
|
||||||
|
# Mark complete
|
||||||
|
task-master set-status --id=2.3 --status=done
|
||||||
|
|
||||||
|
# Repeat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Feature Planning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Write feature spec
|
||||||
|
vim new-feature.txt
|
||||||
|
|
||||||
|
# Generate tasks
|
||||||
|
task-master parse-prd new-feature.txt --num-tasks 10
|
||||||
|
|
||||||
|
# Analyze and expand
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
task-master expand --all --research --force
|
||||||
|
|
||||||
|
# Review and adjust
|
||||||
|
task-master list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern: Sprint Planning
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parse sprint requirements
|
||||||
|
task-master parse-prd sprint-requirements.txt
|
||||||
|
|
||||||
|
# Analyze complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# View report
|
||||||
|
task-master complexity-report
|
||||||
|
|
||||||
|
# Adjust task estimates based on complexity scores
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Read the [Codex CLI Provider Documentation](../providers/codex-cli)
|
||||||
|
- Explore [Configuration Options](../best-practices/configuration-advanced)
|
||||||
|
- Check out [API Keys Setup](../getting-started/api-keys)
|
||||||
|
- Learn about [Task Structure](../capabilities/task-structure)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
|
||||||
@@ -112,6 +112,19 @@ AWS_ACCESS_KEY_ID="your-aws-access-key"
|
|||||||
AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
|
AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### OPENAI_CODEX_API_KEY
|
||||||
|
- **Provider**: Codex CLI
|
||||||
|
- **Required**: ❌ **No** (uses OAuth authentication)
|
||||||
|
- **Purpose**: Optional API key for Codex CLI provider
|
||||||
|
- **Authentication**: OAuth via `codex login` is preferred, API key is optional fallback
|
||||||
|
- **Models**: GPT-5, GPT-5-Codex (subscription required)
|
||||||
|
- **Get Key**: [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Optional - OAuth authentication is preferred
|
||||||
|
OPENAI_CODEX_API_KEY="sk-your-openai-api-key-here"
|
||||||
|
```
|
||||||
|
|
||||||
### CLAUDE_CODE_API_KEY
|
### CLAUDE_CODE_API_KEY
|
||||||
- **Provider**: Claude Code CLI
|
- **Provider**: Claude Code CLI
|
||||||
- **Required**: ❌ **No** (uses OAuth tokens)
|
- **Required**: ❌ **No** (uses OAuth tokens)
|
||||||
|
|||||||
408
apps/docs/providers/codex-cli.mdx
Normal file
408
apps/docs/providers/codex-cli.mdx
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
---
|
||||||
|
title: Codex CLI Provider
|
||||||
|
sidebarTitle: "Codex CLI"
|
||||||
|
---
|
||||||
|
|
||||||
|
The `codex-cli` provider integrates Task Master with OpenAI's Codex CLI via the community AI SDK provider [`ai-sdk-provider-codex-cli`](https://github.com/ben-vargas/ai-sdk-provider-codex-cli). It uses your ChatGPT subscription (OAuth) via `codex login`, with optional `OPENAI_CODEX_API_KEY` support.
|
||||||
|
|
||||||
|
## Why Use Codex CLI?
|
||||||
|
|
||||||
|
The primary benefits of using the `codex-cli` provider include:
|
||||||
|
|
||||||
|
- **Use Latest OpenAI Models**: Access to cutting-edge models like GPT-5 and GPT-5-Codex via ChatGPT subscription
|
||||||
|
- **OAuth Authentication**: No API key management needed - authenticate once with `codex login`
|
||||||
|
- **Built-in Tool Execution**: Native support for command execution, file changes, MCP tools, and web search
|
||||||
|
- **Native JSON Schema Support**: Structured output generation without post-processing
|
||||||
|
- **Approval/Sandbox Modes**: Fine-grained control over command execution and filesystem access for safety
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
Get up and running with Codex CLI in 3 steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install Codex CLI globally
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# 2. Authenticate with your ChatGPT account
|
||||||
|
codex login
|
||||||
|
|
||||||
|
# 3. Configure Task Master to use Codex CLI
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Node.js**: >= 18.0.0
|
||||||
|
- **Codex CLI**: >= 0.42.0 (>= 0.44.0 recommended)
|
||||||
|
- **ChatGPT Subscription**: Required for OAuth access (Plus, Pro, Business, Edu, or Enterprise)
|
||||||
|
- **Task Master**: >= 0.27.3 (version with Codex CLI support)
|
||||||
|
|
||||||
|
### Checking Your Versions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Node.js version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# Check Codex CLI version
|
||||||
|
codex --version
|
||||||
|
|
||||||
|
# Check Task Master version
|
||||||
|
task-master --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install Codex CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally via npm
|
||||||
|
npm install -g @openai/codex
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `v0.44.0` or higher
|
||||||
|
|
||||||
|
### Install Task Master (if not already installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g task-master-ai
|
||||||
|
|
||||||
|
# Or install in your project
|
||||||
|
npm install --save-dev task-master-ai
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### OAuth Authentication (Primary Method - Recommended)
|
||||||
|
|
||||||
|
The Codex CLI provider is designed to use OAuth authentication with your ChatGPT subscription:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Launch Codex CLI and authenticate
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Open a browser window for OAuth authentication
|
||||||
|
2. Prompt you to log in with your ChatGPT account
|
||||||
|
3. Store authentication credentials locally
|
||||||
|
4. Allow Task Master to automatically use these credentials
|
||||||
|
|
||||||
|
To verify your authentication:
|
||||||
|
```bash
|
||||||
|
# Open interactive Codex CLI
|
||||||
|
codex
|
||||||
|
|
||||||
|
# Use /about command to see auth status
|
||||||
|
/about
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional: API Key Method
|
||||||
|
|
||||||
|
While OAuth is the primary and recommended method, you can optionally use an OpenAI API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In your .env file
|
||||||
|
OPENAI_CODEX_API_KEY=sk-your-openai-api-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes**:
|
||||||
|
- The API key will **only** be injected when explicitly provided
|
||||||
|
- OAuth authentication is always preferred when available
|
||||||
|
- Using an API key doesn't provide access to subscription-only models like GPT-5-Codex
|
||||||
|
- For full OpenAI API access with non-subscription models, consider using the standard `openai` provider instead
|
||||||
|
- `OPENAI_CODEX_API_KEY` is specific to the codex-cli provider to avoid conflicts with the `openai` provider's `OPENAI_API_KEY`
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
The Codex CLI provider supports only models available through ChatGPT subscription:
|
||||||
|
|
||||||
|
| Model ID | Description | Max Input Tokens | Max Output Tokens |
|
||||||
|
|----------|-------------|------------------|-------------------|
|
||||||
|
| `gpt-5` | Latest GPT-5 model | 272K | 128K |
|
||||||
|
| `gpt-5-codex` | GPT-5 optimized for agentic software engineering | 272K | 128K |
|
||||||
|
|
||||||
|
**Note**: These models are only available via OAuth subscription through Codex CLI (ChatGPT Plus, Pro, Business, Edu, or Enterprise plans). For other OpenAI models, use the standard `openai` provider with an API key.
|
||||||
|
|
||||||
|
**Research Capabilities**: Both GPT-5 models support web search tools, making them suitable for the `research` role in addition to `main` and `fallback` roles.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
Add Codex CLI to your `.taskmaster/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration with Codex CLI Settings
|
||||||
|
|
||||||
|
The `codexCli` section allows you to customize Codex CLI behavior:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"main": {
|
||||||
|
"provider": "codex-cli",
|
||||||
|
"modelId": "gpt-5-codex",
|
||||||
|
"maxTokens": 128000,
|
||||||
|
"temperature": 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"skipGitRepoCheck": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"sandboxMode": "workspace-write",
|
||||||
|
"verbose": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Codex CLI Settings Reference
|
||||||
|
|
||||||
|
#### Core Settings
|
||||||
|
|
||||||
|
- **`allowNpx`** (boolean, default: `false`)
|
||||||
|
- Allow fallback to `npx @openai/codex` if the CLI is not found on PATH
|
||||||
|
- Useful for CI environments or systems without global npm installations
|
||||||
|
- Example: `"allowNpx": true`
|
||||||
|
|
||||||
|
- **`skipGitRepoCheck`** (boolean, default: `false`)
|
||||||
|
- Skip git repository safety check before execution
|
||||||
|
- Recommended for CI environments or non-repository usage
|
||||||
|
- Example: `"skipGitRepoCheck": true`
|
||||||
|
|
||||||
|
#### Execution Control
|
||||||
|
|
||||||
|
- **`approvalMode`** (string)
|
||||||
|
- Controls when to require user approval for command execution
|
||||||
|
- Options:
|
||||||
|
- `"untrusted"`: Require approval for all commands
|
||||||
|
- `"on-failure"`: Only require approval after a command fails (default)
|
||||||
|
- `"on-request"`: Approve only when explicitly requested
|
||||||
|
- `"never"`: Never require approval (use with caution)
|
||||||
|
- Example: `"approvalMode": "on-failure"`
|
||||||
|
|
||||||
|
- **`sandboxMode`** (string)
|
||||||
|
- Controls filesystem access permissions
|
||||||
|
- Options:
|
||||||
|
- `"read-only"`: Read-only access to filesystem
|
||||||
|
- `"workspace-write"`: Allow writes to workspace directory (default)
|
||||||
|
- `"danger-full-access"`: Full filesystem access (use with extreme caution)
|
||||||
|
- Example: `"sandboxMode": "workspace-write"`
|
||||||
|
|
||||||
|
#### Advanced Settings
|
||||||
|
|
||||||
|
- **`codexPath`** (string, optional)
|
||||||
|
- Custom path to Codex CLI executable
|
||||||
|
- Useful when Codex is installed in a non-standard location
|
||||||
|
- Example: `"codexPath": "/usr/local/bin/codex"`
|
||||||
|
|
||||||
|
- **`cwd`** (string, optional)
|
||||||
|
- Working directory for Codex CLI execution
|
||||||
|
- Defaults to current working directory
|
||||||
|
- Example: `"cwd": "/path/to/project"`
|
||||||
|
|
||||||
|
- **`verbose`** (boolean, optional)
|
||||||
|
- Enable verbose provider logging
|
||||||
|
- Helpful for debugging issues
|
||||||
|
- Example: `"verbose": true`
|
||||||
|
|
||||||
|
- **`fullAuto`** (boolean, optional)
|
||||||
|
- Fully automatic mode (equivalent to `--full-auto` flag)
|
||||||
|
- Bypasses most approvals for fully automated workflows
|
||||||
|
- Example: `"fullAuto": true`
|
||||||
|
|
||||||
|
### Command-Specific Settings
|
||||||
|
|
||||||
|
Override settings for specific Task Master commands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true,
|
||||||
|
"approvalMode": "on-failure",
|
||||||
|
"commandSpecific": {
|
||||||
|
"parse-prd": {
|
||||||
|
"approvalMode": "never",
|
||||||
|
"verbose": true
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
"sandboxMode": "read-only"
|
||||||
|
},
|
||||||
|
"add-task": {
|
||||||
|
"approvalMode": "untrusted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting Codex CLI Models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set Codex CLI for main role
|
||||||
|
task-master models --set-main gpt-5-codex --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI for fallback role
|
||||||
|
task-master models --set-fallback gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Set Codex CLI for research role
|
||||||
|
task-master models --set-research gpt-5 --codex-cli
|
||||||
|
|
||||||
|
# Verify configuration
|
||||||
|
task-master models
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Codex CLI with Task Master Commands
|
||||||
|
|
||||||
|
Once configured, use Task Master commands as normal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parse a PRD with Codex CLI
|
||||||
|
task-master parse-prd my-requirements.txt
|
||||||
|
|
||||||
|
# Analyze project complexity
|
||||||
|
task-master analyze-complexity --research
|
||||||
|
|
||||||
|
# Expand a task into subtasks
|
||||||
|
task-master expand --id=1.2
|
||||||
|
|
||||||
|
# Add a new task with AI assistance
|
||||||
|
task-master add-task --prompt="Implement user authentication" --research
|
||||||
|
```
|
||||||
|
|
||||||
|
The provider will automatically use your OAuth credentials when Codex CLI is configured.
|
||||||
|
|
||||||
|
## Codebase Features
|
||||||
|
|
||||||
|
The Codex CLI provider is **codebase-capable**, meaning it can analyze and interact with your project files. This enables advanced features like:
|
||||||
|
|
||||||
|
- **Code Analysis**: Understanding your project structure and dependencies
|
||||||
|
- **Intelligent Suggestions**: Context-aware task recommendations
|
||||||
|
- **File Operations**: Reading and analyzing project files for better task generation
|
||||||
|
- **Pattern Recognition**: Identifying common patterns and best practices in your codebase
|
||||||
|
|
||||||
|
### Enabling Codebase Analysis
|
||||||
|
|
||||||
|
Codebase analysis is automatically enabled when:
|
||||||
|
1. Your provider is set to `codex-cli`
|
||||||
|
2. `enableCodebaseAnalysis` is `true` in your global configuration (default)
|
||||||
|
|
||||||
|
To verify or configure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"enableCodebaseAnalysis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "codex: command not found" Error
|
||||||
|
|
||||||
|
**Symptoms**: Task Master reports that the Codex CLI is not found.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Install Codex CLI globally**:
|
||||||
|
```bash
|
||||||
|
npm install -g @openai/codex
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify installation**:
|
||||||
|
```bash
|
||||||
|
codex --version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Alternative: Enable npx fallback**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"codexCli": {
|
||||||
|
"allowNpx": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Not logged in" Errors
|
||||||
|
|
||||||
|
**Symptoms**: Authentication errors when trying to use Codex CLI.
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
1. **Authenticate with OAuth**:
|
||||||
|
```bash
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify authentication status**:
|
||||||
|
```bash
|
||||||
|
codex
|
||||||
|
# Then use /about command
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Re-authenticate if needed**:
|
||||||
|
```bash
|
||||||
|
# Logout first
|
||||||
|
codex
|
||||||
|
# Use /auth command to change auth method
|
||||||
|
|
||||||
|
# Then login again
|
||||||
|
codex login
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Model not available" Errors
|
||||||
|
|
||||||
|
**Symptoms**: Error indicating the requested model is not available.
|
||||||
|
|
||||||
|
**Causes and Solutions**:
|
||||||
|
|
||||||
|
1. **Using unsupported model**:
|
||||||
|
- Only `gpt-5` and `gpt-5-codex` are available via Codex CLI
|
||||||
|
- For other OpenAI models, use the standard `openai` provider
|
||||||
|
|
||||||
|
2. **Subscription not active**:
|
||||||
|
- Verify your ChatGPT subscription is active
|
||||||
|
- Check subscription status at <https://platform.openai.com>
|
||||||
|
|
||||||
|
3. **Wrong provider selected**:
|
||||||
|
- Verify you're using `--codex-cli` flag when setting models
|
||||||
|
- Check `.taskmaster/config.json` shows `"provider": "codex-cli"`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **OAuth subscription required**: No API key needed for basic operation, but requires active ChatGPT subscription
|
||||||
|
- **Limited model selection**: Only `gpt-5` and `gpt-5-codex` available via OAuth
|
||||||
|
- **Pricing information**: Not available for OAuth models (shows as "Unknown" in cost calculations)
|
||||||
|
- **No automatic dependency**: The `@openai/codex` package is not added to Task Master's dependencies - install it globally or enable `allowNpx`
|
||||||
|
- **Codebase analysis**: Automatically enabled when using `codex-cli` provider
|
||||||
|
- **Safety first**: Default settings prioritize safety with `approvalMode: "on-failure"` and `sandboxMode: "workspace-write"`
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Configuration Guide](../best-practices/configuration-advanced) - Complete Codex CLI configuration reference
|
||||||
|
- [API Keys](../getting-started/api-keys) - Setting up OPENAI_CODEX_API_KEY
|
||||||
|
- [Codex CLI Usage Examples](../examples/codex-cli-usage) - Practical usage examples
|
||||||
|
- [ai-sdk-provider-codex-cli](https://github.com/ben-vargas/ai-sdk-provider-codex-cli) - Source code for the provider package
|
||||||
@@ -69,29 +69,11 @@ export function resolveTasksPath(args, log = silentLogger) {
|
|||||||
|
|
||||||
// Use core findTasksPath with explicit path and normalized projectRoot context
|
// Use core findTasksPath with explicit path and normalized projectRoot context
|
||||||
if (projectRoot) {
|
if (projectRoot) {
|
||||||
const foundPath = coreFindTasksPath(explicitPath, { projectRoot }, log);
|
return coreFindTasksPath(explicitPath, { projectRoot }, log);
|
||||||
// If core function returns null and no explicit path was provided,
|
|
||||||
// construct the expected default path as documented
|
|
||||||
if (foundPath === null && !explicitPath) {
|
|
||||||
const defaultPath = path.join(
|
|
||||||
projectRoot,
|
|
||||||
'.taskmaster',
|
|
||||||
'tasks',
|
|
||||||
'tasks.json'
|
|
||||||
);
|
|
||||||
log?.info?.(
|
|
||||||
`Core findTasksPath returned null, using default path: ${defaultPath}`
|
|
||||||
);
|
|
||||||
return defaultPath;
|
|
||||||
}
|
|
||||||
return foundPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to core function without projectRoot context
|
// Fallback to core function without projectRoot context
|
||||||
const foundPath = coreFindTasksPath(explicitPath, null, log);
|
return coreFindTasksPath(explicitPath, null, log);
|
||||||
// Note: When no projectRoot is available, we can't construct a default path
|
|
||||||
// so we return null and let the calling code handle the error
|
|
||||||
return foundPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
67
output.txt
Normal file
67
output.txt
Normal file
File diff suppressed because one or more lines are too long
41
package-lock.json
generated
41
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.28.0-rc.1",
|
"version": "0.27.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.28.0-rc.1",
|
"version": "0.27.3",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.5-rc.0",
|
"version": "0.25.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "*"
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
@@ -635,6 +635,7 @@
|
|||||||
"apps/extension/node_modules/zod": {
|
"apps/extension/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -1829,6 +1830,7 @@
|
|||||||
"version": "7.28.4",
|
"version": "7.28.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -2661,6 +2663,7 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -4580,7 +4583,6 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5170,7 +5172,6 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5179,6 +5180,7 @@
|
|||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5469,6 +5471,7 @@
|
|||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5569,6 +5572,7 @@
|
|||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -8588,6 +8592,7 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -8596,6 +8601,7 @@
|
|||||||
"version": "19.1.6",
|
"version": "19.1.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -9041,6 +9047,7 @@
|
|||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -9106,6 +9113,7 @@
|
|||||||
"node_modules/ai": {
|
"node_modules/ai": {
|
||||||
"version": "5.0.57",
|
"version": "5.0.57",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/gateway": "1.0.30",
|
"@ai-sdk/gateway": "1.0.30",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
@@ -9325,6 +9333,7 @@
|
|||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -10330,6 +10339,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
@@ -12193,7 +12203,8 @@
|
|||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1312386",
|
"version": "0.0.1312386",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -12787,6 +12798,7 @@
|
|||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -13099,6 +13111,7 @@
|
|||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -15452,6 +15465,7 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.0",
|
"@alcalzone/ansi-tokenize": "^0.2.0",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -16409,6 +16423,7 @@
|
|||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.7.0",
|
"@jest/core": "^29.7.0",
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
@@ -18026,6 +18041,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
}
|
}
|
||||||
@@ -18351,7 +18367,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -18576,7 +18591,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -18707,6 +18721,7 @@
|
|||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "15.0.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
@@ -21429,6 +21444,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -22811,6 +22827,7 @@
|
|||||||
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.93.0",
|
"@oxc-project/types": "=0.93.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.41",
|
"@rolldown/pluginutils": "1.0.0-beta.41",
|
||||||
@@ -25239,6 +25256,7 @@
|
|||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -25355,6 +25373,7 @@
|
|||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^3.0.0",
|
"@types/unist": "^3.0.0",
|
||||||
"bail": "^2.0.0",
|
"bail": "^2.0.0",
|
||||||
@@ -25797,6 +25816,7 @@
|
|||||||
"version": "5.4.20",
|
"version": "5.4.20",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -25909,7 +25929,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -26493,7 +26512,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.2",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -26636,6 +26655,7 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -27377,6 +27397,7 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class TaskEntity implements Task {
|
|||||||
// Normalize subtask IDs to strings
|
// Normalize subtask IDs to strings
|
||||||
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
||||||
...subtask,
|
...subtask,
|
||||||
id: String(subtask.id),
|
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
|
||||||
parentId: String(subtask.parentId)
|
parentId: String(subtask.parentId)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ export const ERROR_CODES = {
|
|||||||
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
||||||
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'
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ export {
|
|||||||
type ListTasksResult,
|
type ListTasksResult,
|
||||||
type StartTaskOptions,
|
type StartTaskOptions,
|
||||||
type StartTaskResult,
|
type StartTaskResult,
|
||||||
type ConflictCheckResult,
|
type ConflictCheckResult
|
||||||
type ExportTasksOptions,
|
|
||||||
type ExportResult
|
|
||||||
} from './task-master-core.js';
|
} from './task-master-core.js';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
|
|||||||
@@ -5,16 +5,6 @@
|
|||||||
|
|
||||||
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for loading tasks from storage
|
|
||||||
*/
|
|
||||||
export interface LoadTasksOptions {
|
|
||||||
/** Filter tasks by status */
|
|
||||||
status?: TaskStatus;
|
|
||||||
/** Exclude subtasks from loaded tasks (default: false) */
|
|
||||||
excludeSubtasks?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result type for updateTaskStatus operations
|
* Result type for updateTaskStatus operations
|
||||||
*/
|
*/
|
||||||
@@ -31,12 +21,11 @@ export interface UpdateStatusResult {
|
|||||||
*/
|
*/
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
/**
|
/**
|
||||||
* Load all tasks from storage, optionally filtered by tag and other criteria
|
* Load all tasks from storage, optionally filtered by tag
|
||||||
* @param tag - Optional tag to filter tasks by
|
* @param tag - Optional tag to filter tasks by
|
||||||
* @param options - Optional filtering options (status, excludeSubtasks)
|
|
||||||
* @returns Promise that resolves to an array of tasks
|
* @returns Promise that resolves to an array of tasks
|
||||||
*/
|
*/
|
||||||
loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
|
loadTasks(tag?: string): Promise<Task[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a single task by ID
|
* Load a single task by ID
|
||||||
@@ -216,7 +205,7 @@ export abstract class BaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Abstract methods that must be implemented by concrete classes
|
// Abstract methods that must be implemented by concrete classes
|
||||||
abstract loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
|
abstract loadTasks(tag?: string): Promise<Task[]>;
|
||||||
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
|
||||||
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import { TaskMapper } from './TaskMapper.js';
|
|
||||||
import type { Tables } from '../types/database.types.js';
|
|
||||||
|
|
||||||
type TaskRow = Tables<'tasks'>;
|
|
||||||
|
|
||||||
describe('TaskMapper', () => {
|
|
||||||
describe('extractMetadataField', () => {
|
|
||||||
it('should extract string field from metadata', () => {
|
|
||||||
const taskRow: TaskRow = {
|
|
||||||
id: '123',
|
|
||||||
display_id: '1',
|
|
||||||
title: 'Test Task',
|
|
||||||
description: 'Test description',
|
|
||||||
status: 'todo',
|
|
||||||
priority: 'medium',
|
|
||||||
parent_task_id: null,
|
|
||||||
subtask_position: 0,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
metadata: {
|
|
||||||
details: 'Some details',
|
|
||||||
testStrategy: 'Test with unit tests'
|
|
||||||
},
|
|
||||||
complexity: null,
|
|
||||||
assignee_id: null,
|
|
||||||
estimated_hours: null,
|
|
||||||
actual_hours: null,
|
|
||||||
due_date: null,
|
|
||||||
completed_at: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
|
||||||
|
|
||||||
expect(task.details).toBe('Some details');
|
|
||||||
expect(task.testStrategy).toBe('Test with unit tests');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default value when metadata field is missing', () => {
|
|
||||||
const taskRow: TaskRow = {
|
|
||||||
id: '123',
|
|
||||||
display_id: '1',
|
|
||||||
title: 'Test Task',
|
|
||||||
description: 'Test description',
|
|
||||||
status: 'todo',
|
|
||||||
priority: 'medium',
|
|
||||||
parent_task_id: null,
|
|
||||||
subtask_position: 0,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
metadata: {},
|
|
||||||
complexity: null,
|
|
||||||
assignee_id: null,
|
|
||||||
estimated_hours: null,
|
|
||||||
actual_hours: null,
|
|
||||||
due_date: null,
|
|
||||||
completed_at: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
|
||||||
|
|
||||||
expect(task.details).toBe('');
|
|
||||||
expect(task.testStrategy).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default value when metadata is null', () => {
|
|
||||||
const taskRow: TaskRow = {
|
|
||||||
id: '123',
|
|
||||||
display_id: '1',
|
|
||||||
title: 'Test Task',
|
|
||||||
description: 'Test description',
|
|
||||||
status: 'todo',
|
|
||||||
priority: 'medium',
|
|
||||||
parent_task_id: null,
|
|
||||||
subtask_position: 0,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
metadata: null,
|
|
||||||
complexity: null,
|
|
||||||
assignee_id: null,
|
|
||||||
estimated_hours: null,
|
|
||||||
actual_hours: null,
|
|
||||||
due_date: null,
|
|
||||||
completed_at: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
|
||||||
|
|
||||||
expect(task.details).toBe('');
|
|
||||||
expect(task.testStrategy).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default value and warn when metadata field has wrong type', () => {
|
|
||||||
const consoleWarnSpy = vi
|
|
||||||
.spyOn(console, 'warn')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const taskRow: TaskRow = {
|
|
||||||
id: '123',
|
|
||||||
display_id: '1',
|
|
||||||
title: 'Test Task',
|
|
||||||
description: 'Test description',
|
|
||||||
status: 'todo',
|
|
||||||
priority: 'medium',
|
|
||||||
parent_task_id: null,
|
|
||||||
subtask_position: 0,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
metadata: {
|
|
||||||
details: 12345, // Wrong type: number instead of string
|
|
||||||
testStrategy: ['test1', 'test2'] // Wrong type: array instead of string
|
|
||||||
},
|
|
||||||
complexity: null,
|
|
||||||
assignee_id: null,
|
|
||||||
estimated_hours: null,
|
|
||||||
actual_hours: null,
|
|
||||||
due_date: null,
|
|
||||||
completed_at: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
|
||||||
|
|
||||||
// Should use empty string defaults when type doesn't match
|
|
||||||
expect(task.details).toBe('');
|
|
||||||
expect(task.testStrategy).toBe('');
|
|
||||||
|
|
||||||
// Should have logged warnings
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('Type mismatch in metadata field "details"')
|
|
||||||
);
|
|
||||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
'Type mismatch in metadata field "testStrategy"'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
consoleWarnSpy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('mapStatus', () => {
|
|
||||||
it('should map database status to internal status', () => {
|
|
||||||
expect(TaskMapper.mapStatus('todo')).toBe('pending');
|
|
||||||
expect(TaskMapper.mapStatus('in_progress')).toBe('in-progress');
|
|
||||||
expect(TaskMapper.mapStatus('done')).toBe('done');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,32 +2,22 @@ import { Task, Subtask } from '../types/index.js';
|
|||||||
import { Database, Tables } from '../types/database.types.js';
|
import { Database, Tables } from '../types/database.types.js';
|
||||||
|
|
||||||
type TaskRow = Tables<'tasks'>;
|
type TaskRow = Tables<'tasks'>;
|
||||||
|
type DependencyRow = Tables<'task_dependencies'>;
|
||||||
// Legacy type for backward compatibility
|
|
||||||
type DependencyRow = Tables<'task_dependencies'> & {
|
|
||||||
depends_on_task?: { display_id: string } | null;
|
|
||||||
depends_on_task_id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class TaskMapper {
|
export class TaskMapper {
|
||||||
/**
|
/**
|
||||||
* Maps database tasks to internal Task format
|
* Maps database tasks to internal Task format
|
||||||
* @param dbTasks - Array of tasks from database
|
|
||||||
* @param dependencies - Either a Map of task_id to display_ids or legacy array format
|
|
||||||
*/
|
*/
|
||||||
static mapDatabaseTasksToTasks(
|
static mapDatabaseTasksToTasks(
|
||||||
dbTasks: TaskRow[],
|
dbTasks: TaskRow[],
|
||||||
dependencies: Map<string, string[]> | DependencyRow[]
|
dbDependencies: DependencyRow[]
|
||||||
): Task[] {
|
): Task[] {
|
||||||
if (!dbTasks || dbTasks.length === 0) {
|
if (!dbTasks || dbTasks.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle both Map and array formats for backward compatibility
|
// Group dependencies by task_id
|
||||||
const dependenciesByTaskId =
|
const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies);
|
||||||
dependencies instanceof Map
|
|
||||||
? dependencies
|
|
||||||
: this.groupDependenciesByTaskId(dependencies);
|
|
||||||
|
|
||||||
// Separate parent tasks and subtasks
|
// Separate parent tasks and subtasks
|
||||||
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
|
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
|
||||||
@@ -53,23 +43,21 @@ export class TaskMapper {
|
|||||||
): Task {
|
): Task {
|
||||||
// Map subtasks
|
// Map subtasks
|
||||||
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
|
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
|
||||||
id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
|
id: index + 1, // Use numeric ID for subtasks
|
||||||
parentId: dbTask.id,
|
parentId: dbTask.id,
|
||||||
title: subtask.title,
|
title: subtask.title,
|
||||||
description: subtask.description || '',
|
description: subtask.description || '',
|
||||||
status: this.mapStatus(subtask.status),
|
status: this.mapStatus(subtask.status),
|
||||||
priority: this.mapPriority(subtask.priority),
|
priority: this.mapPriority(subtask.priority),
|
||||||
dependencies: dependenciesByTaskId.get(subtask.id) || [],
|
dependencies: dependenciesByTaskId.get(subtask.id) || [],
|
||||||
details: this.extractMetadataField(subtask.metadata, 'details', ''),
|
details: (subtask.metadata as any)?.details || '',
|
||||||
testStrategy: this.extractMetadataField(
|
testStrategy: (subtask.metadata as any)?.testStrategy || '',
|
||||||
subtask.metadata,
|
|
||||||
'testStrategy',
|
|
||||||
''
|
|
||||||
),
|
|
||||||
createdAt: subtask.created_at,
|
createdAt: subtask.created_at,
|
||||||
updatedAt: subtask.updated_at,
|
updatedAt: subtask.updated_at,
|
||||||
assignee: subtask.assignee_id || undefined,
|
assignee: subtask.assignee_id || undefined,
|
||||||
complexity: subtask.complexity ?? undefined
|
complexity: subtask.complexity
|
||||||
|
? this.mapComplexityToInternal(subtask.complexity)
|
||||||
|
: undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -79,25 +67,22 @@ export class TaskMapper {
|
|||||||
status: this.mapStatus(dbTask.status),
|
status: this.mapStatus(dbTask.status),
|
||||||
priority: this.mapPriority(dbTask.priority),
|
priority: this.mapPriority(dbTask.priority),
|
||||||
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
|
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
|
||||||
details: this.extractMetadataField(dbTask.metadata, 'details', ''),
|
details: (dbTask.metadata as any)?.details || '',
|
||||||
testStrategy: this.extractMetadataField(
|
testStrategy: (dbTask.metadata as any)?.testStrategy || '',
|
||||||
dbTask.metadata,
|
|
||||||
'testStrategy',
|
|
||||||
''
|
|
||||||
),
|
|
||||||
subtasks,
|
subtasks,
|
||||||
createdAt: dbTask.created_at,
|
createdAt: dbTask.created_at,
|
||||||
updatedAt: dbTask.updated_at,
|
updatedAt: dbTask.updated_at,
|
||||||
assignee: dbTask.assignee_id || undefined,
|
assignee: dbTask.assignee_id || undefined,
|
||||||
complexity: dbTask.complexity ?? undefined,
|
complexity: dbTask.complexity
|
||||||
|
? this.mapComplexityToInternal(dbTask.complexity)
|
||||||
|
: undefined,
|
||||||
effort: dbTask.estimated_hours || undefined,
|
effort: dbTask.estimated_hours || undefined,
|
||||||
actualEffort: dbTask.actual_hours || undefined
|
actualEffort: dbTask.actual_hours || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups dependencies by task ID (legacy method for backward compatibility)
|
* Groups dependencies by task ID
|
||||||
* @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
|
|
||||||
*/
|
*/
|
||||||
private static groupDependenciesByTaskId(
|
private static groupDependenciesByTaskId(
|
||||||
dependencies: DependencyRow[]
|
dependencies: DependencyRow[]
|
||||||
@@ -107,14 +92,7 @@ export class TaskMapper {
|
|||||||
if (dependencies) {
|
if (dependencies) {
|
||||||
for (const dep of dependencies) {
|
for (const dep of dependencies) {
|
||||||
const deps = dependenciesByTaskId.get(dep.task_id) || [];
|
const deps = dependenciesByTaskId.get(dep.task_id) || [];
|
||||||
// Handle both old format (UUID string) and new format (object with display_id)
|
deps.push(dep.depends_on_task_id);
|
||||||
const dependencyId =
|
|
||||||
typeof dep.depends_on_task === 'object'
|
|
||||||
? dep.depends_on_task?.display_id
|
|
||||||
: dep.depends_on_task_id;
|
|
||||||
if (dependencyId) {
|
|
||||||
deps.push(dependencyId);
|
|
||||||
}
|
|
||||||
dependenciesByTaskId.set(dep.task_id, deps);
|
dependenciesByTaskId.set(dep.task_id, deps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,38 +157,14 @@ export class TaskMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely extracts a field from metadata JSON with runtime type validation
|
* Maps numeric complexity to descriptive complexity
|
||||||
* @param metadata The metadata object (could be null or any type)
|
|
||||||
* @param field The field to extract
|
|
||||||
* @param defaultValue Default value if field doesn't exist
|
|
||||||
* @returns The extracted value if it matches the expected type, otherwise defaultValue
|
|
||||||
*/
|
*/
|
||||||
private static extractMetadataField<T>(
|
private static mapComplexityToInternal(
|
||||||
metadata: unknown,
|
complexity: number
|
||||||
field: string,
|
): Task['complexity'] {
|
||||||
defaultValue: T
|
if (complexity <= 2) return 'simple';
|
||||||
): T {
|
if (complexity <= 5) return 'moderate';
|
||||||
if (!metadata || typeof metadata !== 'object') {
|
if (complexity <= 8) return 'complex';
|
||||||
return defaultValue;
|
return 'very-complex';
|
||||||
}
|
|
||||||
|
|
||||||
const value = (metadata as Record<string, unknown>)[field];
|
|
||||||
|
|
||||||
if (value === undefined) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runtime type validation: ensure value matches the type of defaultValue
|
|
||||||
const expectedType = typeof defaultValue;
|
|
||||||
const actualType = typeof value;
|
|
||||||
|
|
||||||
if (expectedType !== actualType) {
|
|
||||||
console.warn(
|
|
||||||
`Type mismatch in metadata field "${field}": expected ${expectedType}, got ${actualType}. Using default value.`
|
|
||||||
);
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value as T;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
224
packages/tm-core/src/repositories/supabase-task-repository.ts
Normal file
224
packages/tm-core/src/repositories/supabase-task-repository.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import { Task } from '../types/index.js';
|
||||||
|
import { Database } from '../types/database.types.js';
|
||||||
|
import { TaskMapper } from '../mappers/TaskMapper.js';
|
||||||
|
import { AuthManager } from '../auth/auth-manager.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Zod schema for task status validation
|
||||||
|
const TaskStatusSchema = z.enum([
|
||||||
|
'pending',
|
||||||
|
'in-progress',
|
||||||
|
'done',
|
||||||
|
'review',
|
||||||
|
'deferred',
|
||||||
|
'cancelled',
|
||||||
|
'blocked'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zod schema for task updates
|
||||||
|
const TaskUpdateSchema = z
|
||||||
|
.object({
|
||||||
|
title: z.string().min(1).optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
status: TaskStatusSchema.optional(),
|
||||||
|
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
||||||
|
details: z.string().optional(),
|
||||||
|
testStrategy: z.string().optional()
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
|
export class SupabaseTaskRepository {
|
||||||
|
constructor(private supabase: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
async getTasks(_projectId?: string): Promise<Task[]> {
|
||||||
|
// Get the current context to determine briefId
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = authManager.getContext();
|
||||||
|
|
||||||
|
if (!context || !context.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tasks for the brief using the exact query structure
|
||||||
|
const { data: tasks, error } = await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.select(`
|
||||||
|
*,
|
||||||
|
document:document_id (
|
||||||
|
id,
|
||||||
|
document_name,
|
||||||
|
title,
|
||||||
|
description
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('brief_id', context.briefId)
|
||||||
|
.order('position', { ascending: true })
|
||||||
|
.order('subtask_position', { ascending: true })
|
||||||
|
.order('created_at', { ascending: true });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to fetch tasks: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tasks || tasks.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all dependencies for these tasks
|
||||||
|
const taskIds = tasks.map((t: any) => t.id);
|
||||||
|
const { data: depsData, error: depsError } = await this.supabase
|
||||||
|
.from('task_dependencies')
|
||||||
|
.select('*')
|
||||||
|
.in('task_id', taskIds);
|
||||||
|
|
||||||
|
if (depsError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch task dependencies: ${depsError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mapper to convert to internal format
|
||||||
|
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
|
// Get the current context to determine briefId (projectId not used in Supabase context)
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = authManager.getContext();
|
||||||
|
|
||||||
|
if (!context || !context.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.select('*')
|
||||||
|
.eq('brief_id', context.briefId)
|
||||||
|
.eq('display_id', taskId.toUpperCase())
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.code === 'PGRST116') {
|
||||||
|
return null; // Not found
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to fetch task: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dependencies for this task
|
||||||
|
const { data: depsData } = await this.supabase
|
||||||
|
.from('task_dependencies')
|
||||||
|
.select('*')
|
||||||
|
.eq('task_id', taskId);
|
||||||
|
|
||||||
|
// Get subtasks if this is a parent task
|
||||||
|
const { data: subtasksData } = await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.select('*')
|
||||||
|
.eq('parent_task_id', taskId)
|
||||||
|
.order('subtask_position', { ascending: true });
|
||||||
|
|
||||||
|
// Create dependency map
|
||||||
|
const dependenciesByTaskId = new Map<string, string[]>();
|
||||||
|
if (depsData) {
|
||||||
|
dependenciesByTaskId.set(
|
||||||
|
taskId,
|
||||||
|
depsData.map(
|
||||||
|
(d: Database['public']['Tables']['task_dependencies']['Row']) =>
|
||||||
|
d.depends_on_task_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use mapper to convert single task
|
||||||
|
return TaskMapper.mapDatabaseTaskToTask(
|
||||||
|
data,
|
||||||
|
subtasksData || [],
|
||||||
|
dependenciesByTaskId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(
|
||||||
|
projectId: string,
|
||||||
|
taskId: string,
|
||||||
|
updates: Partial<Task>
|
||||||
|
): Promise<Task> {
|
||||||
|
// Get the current context to determine briefId
|
||||||
|
const authManager = AuthManager.getInstance();
|
||||||
|
const context = authManager.getContext();
|
||||||
|
|
||||||
|
if (!context || !context.briefId) {
|
||||||
|
throw new Error(
|
||||||
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate updates using Zod schema
|
||||||
|
try {
|
||||||
|
TaskUpdateSchema.parse(updates);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
const errorMessages = error.issues
|
||||||
|
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||||
|
.join(', ');
|
||||||
|
throw new Error(`Invalid task update data: ${errorMessages}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Task fields to database fields - only include fields that actually exist in the database
|
||||||
|
const dbUpdates: any = {};
|
||||||
|
|
||||||
|
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||||
|
if (updates.description !== undefined)
|
||||||
|
dbUpdates.description = updates.description;
|
||||||
|
if (updates.status !== undefined)
|
||||||
|
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||||
|
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
||||||
|
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
||||||
|
|
||||||
|
// Update the task
|
||||||
|
const { error } = await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.update(dbUpdates)
|
||||||
|
.eq('brief_id', context.briefId)
|
||||||
|
.eq('display_id', taskId.toUpperCase());
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to update task: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated task by fetching it
|
||||||
|
const updatedTask = await this.getTask(projectId, taskId);
|
||||||
|
if (!updatedTask) {
|
||||||
|
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal status to database status
|
||||||
|
*/
|
||||||
|
private mapStatusToDatabase(
|
||||||
|
status: string
|
||||||
|
): Database['public']['Enums']['task_status'] {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return 'todo';
|
||||||
|
case 'in-progress':
|
||||||
|
case 'in_progress': // Accept both formats
|
||||||
|
return 'in_progress';
|
||||||
|
case 'done':
|
||||||
|
return 'done';
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
import { Database } from '../../types/database.types.js';
|
|
||||||
import { DependencyWithDisplayId } from '../../types/repository-types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles fetching and processing of task dependencies with display_ids
|
|
||||||
*/
|
|
||||||
export class DependencyFetcher {
|
|
||||||
constructor(private supabase: SupabaseClient<Database>) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches dependencies for given task IDs with display_ids joined
|
|
||||||
* @param taskIds Array of task IDs to fetch dependencies for
|
|
||||||
* @returns Map of task ID to array of dependency display_ids
|
|
||||||
*/
|
|
||||||
async fetchDependenciesWithDisplayIds(
|
|
||||||
taskIds: string[]
|
|
||||||
): Promise<Map<string, string[]>> {
|
|
||||||
if (!taskIds || taskIds.length === 0) {
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
|
||||||
.from('task_dependencies')
|
|
||||||
.select(`
|
|
||||||
task_id,
|
|
||||||
depends_on_task:tasks!task_dependencies_depends_on_task_id_fkey (
|
|
||||||
display_id
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.in('task_id', taskIds);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(`Failed to fetch task dependencies: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.processDependencyData(data as DependencyWithDisplayId[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes raw dependency data into a map structure
|
|
||||||
*/
|
|
||||||
private processDependencyData(
|
|
||||||
dependencies: DependencyWithDisplayId[]
|
|
||||||
): Map<string, string[]> {
|
|
||||||
const dependenciesByTaskId = new Map<string, string[]>();
|
|
||||||
|
|
||||||
if (!dependencies) {
|
|
||||||
return dependenciesByTaskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dep of dependencies) {
|
|
||||||
if (!dep.task_id) continue;
|
|
||||||
|
|
||||||
const currentDeps = dependenciesByTaskId.get(dep.task_id) || [];
|
|
||||||
|
|
||||||
// Extract display_id from the joined object
|
|
||||||
const displayId = dep.depends_on_task?.display_id;
|
|
||||||
if (displayId) {
|
|
||||||
currentDeps.push(displayId);
|
|
||||||
}
|
|
||||||
|
|
||||||
dependenciesByTaskId.set(dep.task_id, currentDeps);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependenciesByTaskId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* Supabase repository implementations
|
|
||||||
*/
|
|
||||||
export { SupabaseTaskRepository } from './supabase-task-repository.js';
|
|
||||||
export { DependencyFetcher } from './dependency-fetcher.js';
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
import { Task } from '../../types/index.js';
|
|
||||||
import { Database, Json } from '../../types/database.types.js';
|
|
||||||
import { TaskMapper } from '../../mappers/TaskMapper.js';
|
|
||||||
import { AuthManager } from '../../auth/auth-manager.js';
|
|
||||||
import { DependencyFetcher } from './dependency-fetcher.js';
|
|
||||||
import {
|
|
||||||
TaskWithRelations,
|
|
||||||
TaskDatabaseUpdate
|
|
||||||
} from '../../types/repository-types.js';
|
|
||||||
import { LoadTasksOptions } from '../../interfaces/storage.interface.js';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
// Zod schema for task status validation
|
|
||||||
const TaskStatusSchema = z.enum([
|
|
||||||
'pending',
|
|
||||||
'in-progress',
|
|
||||||
'done',
|
|
||||||
'review',
|
|
||||||
'deferred',
|
|
||||||
'cancelled',
|
|
||||||
'blocked'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Zod schema for task updates
|
|
||||||
const TaskUpdateSchema = z
|
|
||||||
.object({
|
|
||||||
title: z.string().min(1).optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
status: TaskStatusSchema.optional(),
|
|
||||||
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
|
|
||||||
details: z.string().optional(),
|
|
||||||
testStrategy: z.string().optional()
|
|
||||||
})
|
|
||||||
.partial();
|
|
||||||
|
|
||||||
export class SupabaseTaskRepository {
|
|
||||||
private dependencyFetcher: DependencyFetcher;
|
|
||||||
private authManager: AuthManager;
|
|
||||||
|
|
||||||
constructor(private supabase: SupabaseClient<Database>) {
|
|
||||||
this.dependencyFetcher = new DependencyFetcher(supabase);
|
|
||||||
this.authManager = AuthManager.getInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current brief ID from auth context
|
|
||||||
* @throws {Error} If no brief is selected
|
|
||||||
*/
|
|
||||||
private getBriefIdOrThrow(): string {
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
if (!context?.briefId) {
|
|
||||||
throw new Error(
|
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context.briefId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTasks(
|
|
||||||
_projectId?: string,
|
|
||||||
options?: LoadTasksOptions
|
|
||||||
): Promise<Task[]> {
|
|
||||||
const briefId = this.getBriefIdOrThrow();
|
|
||||||
|
|
||||||
// Build query with filters
|
|
||||||
let query = this.supabase
|
|
||||||
.from('tasks')
|
|
||||||
.select(`
|
|
||||||
*,
|
|
||||||
document:document_id (
|
|
||||||
id,
|
|
||||||
document_name,
|
|
||||||
title,
|
|
||||||
description
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq('brief_id', briefId);
|
|
||||||
|
|
||||||
// Apply status filter at database level if specified
|
|
||||||
if (options?.status) {
|
|
||||||
const dbStatus = this.mapStatusToDatabase(options.status);
|
|
||||||
query = query.eq('status', dbStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply subtask exclusion at database level if specified
|
|
||||||
if (options?.excludeSubtasks) {
|
|
||||||
// Only fetch parent tasks (where parent_task_id is null)
|
|
||||||
query = query.is('parent_task_id', null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute query with ordering
|
|
||||||
const { data: tasks, error } = await query
|
|
||||||
.order('position', { ascending: true })
|
|
||||||
.order('subtask_position', { ascending: true })
|
|
||||||
.order('created_at', { ascending: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(`Failed to fetch tasks: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tasks || tasks.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type-safe task ID extraction
|
|
||||||
const typedTasks = tasks as TaskWithRelations[];
|
|
||||||
const taskIds = typedTasks.map((t) => t.id);
|
|
||||||
const dependenciesMap =
|
|
||||||
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(taskIds);
|
|
||||||
|
|
||||||
// Use mapper to convert to internal format
|
|
||||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, dependenciesMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
|
||||||
const briefId = this.getBriefIdOrThrow();
|
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
|
||||||
.from('tasks')
|
|
||||||
.select('*')
|
|
||||||
.eq('brief_id', briefId)
|
|
||||||
.eq('display_id', taskId.toUpperCase())
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
if (error.code === 'PGRST116') {
|
|
||||||
return null; // Not found
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch task: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get subtasks if this is a parent task
|
|
||||||
const { data: subtasksData } = await this.supabase
|
|
||||||
.from('tasks')
|
|
||||||
.select('*')
|
|
||||||
.eq('parent_task_id', data.id)
|
|
||||||
.order('subtask_position', { ascending: true });
|
|
||||||
|
|
||||||
// Get all task IDs (parent + subtasks) to fetch dependencies
|
|
||||||
const allTaskIds = [data.id, ...(subtasksData?.map((st) => st.id) || [])];
|
|
||||||
|
|
||||||
// Fetch dependencies using the dedicated fetcher
|
|
||||||
const dependenciesByTaskId =
|
|
||||||
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(allTaskIds);
|
|
||||||
|
|
||||||
// Use mapper to convert single task
|
|
||||||
return TaskMapper.mapDatabaseTaskToTask(
|
|
||||||
data,
|
|
||||||
subtasksData || [],
|
|
||||||
dependenciesByTaskId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTask(
|
|
||||||
projectId: string,
|
|
||||||
taskId: string,
|
|
||||||
updates: Partial<Task>
|
|
||||||
): Promise<Task> {
|
|
||||||
const briefId = this.getBriefIdOrThrow();
|
|
||||||
|
|
||||||
// Validate updates using Zod schema
|
|
||||||
try {
|
|
||||||
TaskUpdateSchema.parse(updates);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
const errorMessages = error.issues
|
|
||||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
|
||||||
.join(', ');
|
|
||||||
throw new Error(`Invalid task update data: ${errorMessages}`);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert Task fields to database fields with proper typing
|
|
||||||
const dbUpdates: TaskDatabaseUpdate = {};
|
|
||||||
|
|
||||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
|
||||||
if (updates.description !== undefined)
|
|
||||||
dbUpdates.description = updates.description;
|
|
||||||
if (updates.status !== undefined)
|
|
||||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
|
||||||
if (updates.priority !== undefined)
|
|
||||||
dbUpdates.priority = this.mapPriorityToDatabase(updates.priority);
|
|
||||||
|
|
||||||
// Handle metadata fields (details, testStrategy, etc.)
|
|
||||||
// Load existing metadata to preserve fields not being updated
|
|
||||||
const { data: existingMetadataRow, error: existingMetadataError } =
|
|
||||||
await this.supabase
|
|
||||||
.from('tasks')
|
|
||||||
.select('metadata')
|
|
||||||
.eq('brief_id', briefId)
|
|
||||||
.eq('display_id', taskId.toUpperCase())
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (existingMetadataError) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to load existing task metadata: ${existingMetadataError.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata: Record<string, unknown> = {
|
|
||||||
...((existingMetadataRow?.metadata as Record<string, unknown>) ?? {})
|
|
||||||
};
|
|
||||||
|
|
||||||
if (updates.details !== undefined) metadata.details = updates.details;
|
|
||||||
if (updates.testStrategy !== undefined)
|
|
||||||
metadata.testStrategy = updates.testStrategy;
|
|
||||||
|
|
||||||
if (Object.keys(metadata).length > 0) {
|
|
||||||
dbUpdates.metadata = metadata as Json;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the task
|
|
||||||
const { error } = await this.supabase
|
|
||||||
.from('tasks')
|
|
||||||
.update(dbUpdates)
|
|
||||||
.eq('brief_id', briefId)
|
|
||||||
.eq('display_id', taskId.toUpperCase());
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw new Error(`Failed to update task: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the updated task by fetching it
|
|
||||||
const updatedTask = await this.getTask(projectId, taskId);
|
|
||||||
if (!updatedTask) {
|
|
||||||
throw new Error(`Failed to retrieve updated task ${taskId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps internal status to database status
|
|
||||||
*/
|
|
||||||
private mapStatusToDatabase(
|
|
||||||
status: string
|
|
||||||
): Database['public']['Enums']['task_status'] {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
return 'todo';
|
|
||||||
case 'in-progress':
|
|
||||||
case 'in_progress': // Accept both formats
|
|
||||||
return 'in_progress';
|
|
||||||
case 'done':
|
|
||||||
return 'done';
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps internal priority to database priority
|
|
||||||
* Task Master uses 'critical', database uses 'urgent'
|
|
||||||
*/
|
|
||||||
private mapPriorityToDatabase(
|
|
||||||
priority: string
|
|
||||||
): Database['public']['Enums']['task_priority'] {
|
|
||||||
switch (priority) {
|
|
||||||
case 'critical':
|
|
||||||
return 'urgent';
|
|
||||||
case 'low':
|
|
||||||
case 'medium':
|
|
||||||
case 'high':
|
|
||||||
return priority as Database['public']['Enums']['task_priority'];
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Invalid task priority: ${priority}. Valid priorities are: low, medium, high, critical`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Task, TaskTag } from '../types/index.js';
|
import { Task, TaskTag } from '../types/index.js';
|
||||||
import { LoadTasksOptions } from '../interfaces/storage.interface.js';
|
|
||||||
|
|
||||||
export interface TaskRepository {
|
export interface TaskRepository {
|
||||||
// Task operations
|
// Task operations
|
||||||
getTasks(projectId: string, options?: LoadTasksOptions): Promise<Task[]>;
|
getTasks(projectId: string): Promise<Task[]>;
|
||||||
getTask(projectId: string, taskId: string): Promise<Task | null>;
|
getTask(projectId: string, taskId: string): Promise<Task | null>;
|
||||||
createTask(projectId: string, task: Omit<Task, 'id'>): Promise<Task>;
|
createTask(projectId: string, task: Omit<Task, 'id'>): Promise<Task>;
|
||||||
updateTask(
|
updateTask(
|
||||||
|
|||||||
@@ -1,496 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Export Service
|
|
||||||
* Core service for exporting tasks to external systems (e.g., Hamster briefs)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Task, TaskStatus } from '../types/index.js';
|
|
||||||
import type { UserContext } from '../auth/types.js';
|
|
||||||
import { ConfigManager } from '../config/config-manager.js';
|
|
||||||
import { AuthManager } from '../auth/auth-manager.js';
|
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
|
||||||
import { FileStorage } from '../storage/file-storage/index.js';
|
|
||||||
|
|
||||||
// Type definitions for the bulk API response
|
|
||||||
interface TaskImportResult {
|
|
||||||
externalId?: string;
|
|
||||||
index: number;
|
|
||||||
success: boolean;
|
|
||||||
taskId?: string;
|
|
||||||
error?: string;
|
|
||||||
validationErrors?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BulkTasksResponse {
|
|
||||||
dryRun: boolean;
|
|
||||||
totalTasks: number;
|
|
||||||
successCount: number;
|
|
||||||
failedCount: number;
|
|
||||||
skippedCount: number;
|
|
||||||
results: TaskImportResult[];
|
|
||||||
summary: {
|
|
||||||
message: string;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for exporting tasks
|
|
||||||
*/
|
|
||||||
export interface ExportTasksOptions {
|
|
||||||
/** Optional tag to export tasks from (uses active tag if not provided) */
|
|
||||||
tag?: string;
|
|
||||||
/** Brief ID to export to */
|
|
||||||
briefId?: string;
|
|
||||||
/** Organization ID (required if briefId is provided) */
|
|
||||||
orgId?: string;
|
|
||||||
/** Filter by task status */
|
|
||||||
status?: TaskStatus;
|
|
||||||
/** Exclude subtasks from export (default: false, subtasks included by default) */
|
|
||||||
excludeSubtasks?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of the export operation
|
|
||||||
*/
|
|
||||||
export interface ExportResult {
|
|
||||||
/** Whether the export was successful */
|
|
||||||
success: boolean;
|
|
||||||
/** Number of tasks exported */
|
|
||||||
taskCount: number;
|
|
||||||
/** The brief ID tasks were exported to */
|
|
||||||
briefId: string;
|
|
||||||
/** The organization ID */
|
|
||||||
orgId: string;
|
|
||||||
/** Optional message */
|
|
||||||
message?: string;
|
|
||||||
/** Error details if export failed */
|
|
||||||
error?: {
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brief information from API
|
|
||||||
*/
|
|
||||||
export interface Brief {
|
|
||||||
id: string;
|
|
||||||
accountId: string;
|
|
||||||
createdAt: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExportService handles task export to external systems
|
|
||||||
*/
|
|
||||||
export class ExportService {
|
|
||||||
private configManager: ConfigManager;
|
|
||||||
private authManager: AuthManager;
|
|
||||||
|
|
||||||
constructor(configManager: ConfigManager, authManager: AuthManager) {
|
|
||||||
this.configManager = configManager;
|
|
||||||
this.authManager = authManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export tasks to a brief
|
|
||||||
*/
|
|
||||||
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
|
|
||||||
// Validate authentication
|
|
||||||
if (!this.authManager.isAuthenticated()) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Authentication required for export',
|
|
||||||
ERROR_CODES.AUTHENTICATION_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current context
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
|
|
||||||
// Determine org and brief IDs
|
|
||||||
let orgId = options.orgId || context?.orgId;
|
|
||||||
let briefId = options.briefId || context?.briefId;
|
|
||||||
|
|
||||||
// Validate we have necessary IDs
|
|
||||||
if (!orgId) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Organization ID is required for export. Use "tm context org" to select one.',
|
|
||||||
ERROR_CODES.MISSING_CONFIGURATION
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!briefId) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Brief ID is required for export. Use "tm context brief" or provide --brief flag.',
|
|
||||||
ERROR_CODES.MISSING_CONFIGURATION
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tasks from the specified or active tag
|
|
||||||
const activeTag = this.configManager.getActiveTag();
|
|
||||||
const tag = options.tag || activeTag;
|
|
||||||
|
|
||||||
// Always read tasks from local file storage for export
|
|
||||||
// (we're exporting local tasks to a remote brief)
|
|
||||||
const fileStorage = new FileStorage(this.configManager.getProjectRoot());
|
|
||||||
await fileStorage.initialize();
|
|
||||||
|
|
||||||
// Load tasks with filters applied at storage layer
|
|
||||||
const filteredTasks = await fileStorage.loadTasks(tag, {
|
|
||||||
status: options.status,
|
|
||||||
excludeSubtasks: options.excludeSubtasks
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get total count (without filters) for comparison
|
|
||||||
const allTasks = await fileStorage.loadTasks(tag);
|
|
||||||
|
|
||||||
const taskListResult = {
|
|
||||||
tasks: filteredTasks,
|
|
||||||
total: allTasks.length,
|
|
||||||
filtered: filteredTasks.length,
|
|
||||||
tag,
|
|
||||||
storageType: 'file' as const
|
|
||||||
};
|
|
||||||
|
|
||||||
if (taskListResult.tasks.length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
taskCount: 0,
|
|
||||||
briefId,
|
|
||||||
orgId,
|
|
||||||
message: 'No tasks found to export',
|
|
||||||
error: {
|
|
||||||
code: 'NO_TASKS',
|
|
||||||
message: 'No tasks match the specified criteria'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call the export API with the original tasks
|
|
||||||
// performExport will handle the transformation based on the method used
|
|
||||||
await this.performExport(orgId, briefId, taskListResult.tasks);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
taskCount: taskListResult.tasks.length,
|
|
||||||
briefId,
|
|
||||||
orgId,
|
|
||||||
message: `Successfully exported ${taskListResult.tasks.length} task(s) to brief`
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
taskCount: 0,
|
|
||||||
briefId,
|
|
||||||
orgId,
|
|
||||||
error: {
|
|
||||||
code: 'EXPORT_FAILED',
|
|
||||||
message: errorMessage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export tasks from a brief ID or URL
|
|
||||||
*/
|
|
||||||
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
|
|
||||||
// Extract brief ID from input
|
|
||||||
const briefId = this.extractBriefId(briefInput);
|
|
||||||
if (!briefId) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Invalid brief ID or URL provided',
|
|
||||||
ERROR_CODES.VALIDATION_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch brief to get organization
|
|
||||||
const brief = await this.authManager.getBrief(briefId);
|
|
||||||
if (!brief) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
'Brief not found or you do not have access',
|
|
||||||
ERROR_CODES.NOT_FOUND
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export with the resolved org and brief
|
|
||||||
return this.exportTasks({
|
|
||||||
orgId: brief.accountId,
|
|
||||||
briefId: brief.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate export context before prompting
|
|
||||||
*/
|
|
||||||
async validateContext(): Promise<{
|
|
||||||
hasOrg: boolean;
|
|
||||||
hasBrief: boolean;
|
|
||||||
context: UserContext | null;
|
|
||||||
}> {
|
|
||||||
const context = this.authManager.getContext();
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasOrg: !!context?.orgId,
|
|
||||||
hasBrief: !!context?.briefId,
|
|
||||||
context
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform tasks for API bulk import format (flat structure)
|
|
||||||
*/
|
|
||||||
private transformTasksForBulkImport(tasks: Task[]): any[] {
|
|
||||||
const flatTasks: any[] = [];
|
|
||||||
|
|
||||||
// Process each task and its subtasks
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
// Add parent task
|
|
||||||
flatTasks.push({
|
|
||||||
externalId: String(task.id),
|
|
||||||
title: task.title,
|
|
||||||
description: this.enrichDescription(task),
|
|
||||||
status: this.mapStatusForAPI(task.status),
|
|
||||||
priority: task.priority || 'medium',
|
|
||||||
dependencies: task.dependencies?.map(String) || [],
|
|
||||||
details: task.details,
|
|
||||||
testStrategy: task.testStrategy,
|
|
||||||
complexity: task.complexity,
|
|
||||||
metadata: {
|
|
||||||
complexity: task.complexity,
|
|
||||||
originalId: task.id,
|
|
||||||
originalDescription: task.description,
|
|
||||||
originalDetails: task.details,
|
|
||||||
originalTestStrategy: task.testStrategy
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subtasks if they exist
|
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
|
||||||
task.subtasks.forEach((subtask) => {
|
|
||||||
flatTasks.push({
|
|
||||||
externalId: `${task.id}.${subtask.id}`,
|
|
||||||
parentExternalId: String(task.id),
|
|
||||||
title: subtask.title,
|
|
||||||
description: this.enrichDescription(subtask),
|
|
||||||
status: this.mapStatusForAPI(subtask.status),
|
|
||||||
priority: subtask.priority || 'medium',
|
|
||||||
dependencies:
|
|
||||||
subtask.dependencies?.map((dep) => {
|
|
||||||
// Convert subtask dependencies to full ID format
|
|
||||||
if (String(dep).includes('.')) {
|
|
||||||
return String(dep);
|
|
||||||
}
|
|
||||||
return `${task.id}.${dep}`;
|
|
||||||
}) || [],
|
|
||||||
details: subtask.details,
|
|
||||||
testStrategy: subtask.testStrategy,
|
|
||||||
complexity: subtask.complexity,
|
|
||||||
metadata: {
|
|
||||||
complexity: subtask.complexity,
|
|
||||||
originalId: subtask.id,
|
|
||||||
originalDescription: subtask.description,
|
|
||||||
originalDetails: subtask.details,
|
|
||||||
originalTestStrategy: subtask.testStrategy
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return flatTasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enrich task/subtask description with implementation details and test strategy
|
|
||||||
* Creates a comprehensive markdown-formatted description
|
|
||||||
*/
|
|
||||||
private enrichDescription(taskOrSubtask: Task | any): string {
|
|
||||||
const sections: string[] = [];
|
|
||||||
|
|
||||||
// Start with original description if it exists
|
|
||||||
if (taskOrSubtask.description) {
|
|
||||||
sections.push(taskOrSubtask.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add implementation details section
|
|
||||||
if (taskOrSubtask.details) {
|
|
||||||
sections.push('## Implementation Details\n');
|
|
||||||
sections.push(taskOrSubtask.details);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add test strategy section
|
|
||||||
if (taskOrSubtask.testStrategy) {
|
|
||||||
sections.push('## Test Strategy\n');
|
|
||||||
sections.push(taskOrSubtask.testStrategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join sections with double newlines for better markdown formatting
|
|
||||||
return sections.join('\n\n').trim() || 'No description provided';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map internal status to API status format
|
|
||||||
*/
|
|
||||||
private mapStatusForAPI(status?: string): string {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
return 'todo';
|
|
||||||
case 'in-progress':
|
|
||||||
return 'in_progress';
|
|
||||||
case 'done':
|
|
||||||
return 'done';
|
|
||||||
default:
|
|
||||||
return 'todo';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform the actual export API call
|
|
||||||
*/
|
|
||||||
private async performExport(
|
|
||||||
orgId: string,
|
|
||||||
briefId: string,
|
|
||||||
tasks: any[]
|
|
||||||
): Promise<void> {
|
|
||||||
// Check if we should use the API endpoint or direct Supabase
|
|
||||||
const useAPIEndpoint = process.env.TM_PUBLIC_BASE_DOMAIN;
|
|
||||||
|
|
||||||
if (useAPIEndpoint) {
|
|
||||||
// Use the new bulk import API endpoint
|
|
||||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
|
|
||||||
|
|
||||||
// Transform tasks to flat structure for API
|
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
|
||||||
|
|
||||||
// Prepare request body
|
|
||||||
const requestBody = {
|
|
||||||
source: 'task-master-cli',
|
|
||||||
accountId: orgId,
|
|
||||||
options: {
|
|
||||||
dryRun: false,
|
|
||||||
stopOnError: false
|
|
||||||
},
|
|
||||||
tasks: flatTasks
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get auth token
|
|
||||||
const credentials = this.authManager.getCredentials();
|
|
||||||
if (!credentials || !credentials.token) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make API request
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${credentials.token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(
|
|
||||||
`API request failed: ${response.status} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = (await response.json()) as BulkTasksResponse;
|
|
||||||
|
|
||||||
if (result.failedCount > 0) {
|
|
||||||
const failedTasks = result.results
|
|
||||||
.filter((r) => !r.success)
|
|
||||||
.map((r) => `${r.externalId}: ${r.error}`)
|
|
||||||
.join(', ');
|
|
||||||
console.warn(
|
|
||||||
`Warning: ${result.failedCount} tasks failed to import: ${failedTasks}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Successfully exported ${result.successCount} of ${result.totalTasks} tasks to brief ${briefId}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Direct Supabase approach is no longer supported
|
|
||||||
// The extractTasks method has been removed from SupabaseTaskRepository
|
|
||||||
// as we now exclusively use the API endpoint for exports
|
|
||||||
throw new Error(
|
|
||||||
'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a brief ID from raw input (ID or URL)
|
|
||||||
*/
|
|
||||||
private extractBriefId(input: string): string | null {
|
|
||||||
const raw = input?.trim() ?? '';
|
|
||||||
if (!raw) return null;
|
|
||||||
|
|
||||||
const parseUrl = (s: string): URL | null => {
|
|
||||||
try {
|
|
||||||
return new URL(s);
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
return new URL(`https://${s}`);
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fromParts = (path: string): string | null => {
|
|
||||||
const parts = path.split('/').filter(Boolean);
|
|
||||||
const briefsIdx = parts.lastIndexOf('briefs');
|
|
||||||
const candidate =
|
|
||||||
briefsIdx >= 0 && parts.length > briefsIdx + 1
|
|
||||||
? parts[briefsIdx + 1]
|
|
||||||
: parts[parts.length - 1];
|
|
||||||
return candidate?.trim() || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to parse as URL
|
|
||||||
const url = parseUrl(raw);
|
|
||||||
if (url) {
|
|
||||||
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
|
|
||||||
const candidate = (qId || fromParts(url.pathname)) ?? null;
|
|
||||||
if (candidate) {
|
|
||||||
if (this.isLikelyId(candidate) || candidate.length >= 8) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it looks like a path without scheme
|
|
||||||
if (raw.includes('/')) {
|
|
||||||
const candidate = fromParts(raw);
|
|
||||||
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return as-is if it looks like an ID
|
|
||||||
if (this.isLikelyId(raw) || raw.length >= 8) {
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string looks like a brief ID (UUID-like)
|
|
||||||
*/
|
|
||||||
private isLikelyId(value: string): boolean {
|
|
||||||
const uuidRegex =
|
|
||||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
||||||
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
||||||
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
|
|
||||||
return (
|
|
||||||
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,4 @@
|
|||||||
|
|
||||||
export { TaskService } from './task-service.js';
|
export { TaskService } from './task-service.js';
|
||||||
export { OrganizationService } from './organization.service.js';
|
export { OrganizationService } from './organization.service.js';
|
||||||
export { ExportService } from './export.service.js';
|
|
||||||
export type { Organization, Brief } from './organization.service.js';
|
export type { Organization, Brief } from './organization.service.js';
|
||||||
export type {
|
|
||||||
ExportTasksOptions,
|
|
||||||
ExportResult
|
|
||||||
} from './export.service.js';
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { ConfigManager } from '../config/config-manager.js';
|
|||||||
import { StorageFactory } from '../storage/storage-factory.js';
|
import { StorageFactory } from '../storage/storage-factory.js';
|
||||||
import { TaskEntity } from '../entities/task.entity.js';
|
import { TaskEntity } from '../entities/task.entity.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import { getLogger } from '../logger/factory.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result returned by getTaskList
|
* Result returned by getTaskList
|
||||||
@@ -52,7 +51,6 @@ export class TaskService {
|
|||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private storage: IStorage;
|
private storage: IStorage;
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private logger = getLogger('TaskService');
|
|
||||||
|
|
||||||
constructor(configManager: ConfigManager) {
|
constructor(configManager: ConfigManager) {
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
@@ -92,76 +90,37 @@ export class TaskService {
|
|||||||
const tag = options.tag || activeTag;
|
const tag = options.tag || activeTag;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Determine if we can push filters to storage layer
|
// Load raw tasks from storage - storage only knows about tags
|
||||||
const canPushStatusFilter =
|
const rawTasks = await this.storage.loadTasks(tag);
|
||||||
options.filter?.status &&
|
|
||||||
!options.filter.priority &&
|
|
||||||
!options.filter.tags &&
|
|
||||||
!options.filter.assignee &&
|
|
||||||
!options.filter.search &&
|
|
||||||
options.filter.hasSubtasks === undefined;
|
|
||||||
|
|
||||||
// Build storage-level options
|
|
||||||
const storageOptions: any = {};
|
|
||||||
|
|
||||||
// Push status filter to storage if it's the only filter
|
|
||||||
if (canPushStatusFilter) {
|
|
||||||
const statuses = Array.isArray(options.filter!.status)
|
|
||||||
? options.filter!.status
|
|
||||||
: [options.filter!.status];
|
|
||||||
// Only push single status to storage (multiple statuses need in-memory filtering)
|
|
||||||
if (statuses.length === 1) {
|
|
||||||
storageOptions.status = statuses[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push subtask exclusion to storage
|
|
||||||
if (options.includeSubtasks === false) {
|
|
||||||
storageOptions.excludeSubtasks = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tasks from storage with pushed-down filters
|
|
||||||
const rawTasks = await this.storage.loadTasks(tag, storageOptions);
|
|
||||||
|
|
||||||
// Get total count without status filters, but preserve subtask exclusion
|
|
||||||
const baseOptions: any = {};
|
|
||||||
if (options.includeSubtasks === false) {
|
|
||||||
baseOptions.excludeSubtasks = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTasks =
|
|
||||||
storageOptions.status !== undefined
|
|
||||||
? await this.storage.loadTasks(tag, baseOptions)
|
|
||||||
: rawTasks;
|
|
||||||
|
|
||||||
// Convert to TaskEntity for business logic operations
|
// Convert to TaskEntity for business logic operations
|
||||||
const taskEntities = TaskEntity.fromArray(rawTasks);
|
const taskEntities = TaskEntity.fromArray(rawTasks);
|
||||||
|
|
||||||
// Apply remaining filters in-memory if needed
|
// Apply filters if provided
|
||||||
let filteredEntities = taskEntities;
|
let filteredEntities = taskEntities;
|
||||||
if (options.filter && !canPushStatusFilter) {
|
if (options.filter) {
|
||||||
filteredEntities = this.applyFilters(taskEntities, options.filter);
|
|
||||||
} else if (
|
|
||||||
options.filter?.status &&
|
|
||||||
Array.isArray(options.filter.status) &&
|
|
||||||
options.filter.status.length > 1
|
|
||||||
) {
|
|
||||||
// Multiple statuses - filter in-memory
|
|
||||||
filteredEntities = this.applyFilters(taskEntities, options.filter);
|
filteredEntities = this.applyFilters(taskEntities, options.filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert back to plain objects
|
// Convert back to plain objects
|
||||||
const tasks = filteredEntities.map((entity) => entity.toJSON());
|
let tasks = filteredEntities.map((entity) => entity.toJSON());
|
||||||
|
|
||||||
|
// Handle subtasks option
|
||||||
|
if (options.includeSubtasks === false) {
|
||||||
|
tasks = tasks.map((task) => ({
|
||||||
|
...task,
|
||||||
|
subtasks: []
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tasks,
|
tasks,
|
||||||
total: allTasks.length,
|
total: rawTasks.length,
|
||||||
filtered: filteredEntities.length,
|
filtered: filteredEntities.length,
|
||||||
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
|
tag: tag, // Return the actual tag being used (either explicitly provided or active tag)
|
||||||
storageType: this.getStorageType()
|
storageType: this.getStorageType()
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (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',
|
||||||
ERROR_CODES.INTERNAL_ERROR,
|
ERROR_CODES.INTERNAL_ERROR,
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats,
|
StorageStats,
|
||||||
UpdateStatusResult,
|
UpdateStatusResult
|
||||||
LoadTasksOptions
|
|
||||||
} from '../interfaces/storage.interface.js';
|
} from '../interfaces/storage.interface.js';
|
||||||
import type {
|
import type {
|
||||||
Task,
|
Task,
|
||||||
@@ -17,7 +16,7 @@ import type {
|
|||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||||
import { SupabaseTaskRepository } from '../repositories/supabase/index.js';
|
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
import { AuthManager } from '../auth/auth-manager.js';
|
import { AuthManager } from '../auth/auth-manager.js';
|
||||||
|
|
||||||
@@ -147,7 +146,7 @@ export class ApiStorage implements IStorage {
|
|||||||
* Load tasks from API
|
* Load tasks from API
|
||||||
* In our system, the tag parameter represents a brief ID
|
* In our system, the tag parameter represents a brief ID
|
||||||
*/
|
*/
|
||||||
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
|
async loadTasks(tag?: string): Promise<Task[]> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -161,9 +160,9 @@ export class ApiStorage implements IStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load tasks from the current brief context with filters pushed to repository
|
// Load tasks from the current brief context
|
||||||
const tasks = await this.retryOperation(() =>
|
const tasks = await this.retryOperation(() =>
|
||||||
this.repository.getTasks(this.projectId, options)
|
this.repository.getTasks(this.projectId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update the tag cache with the loaded task IDs
|
// Update the tag cache with the loaded task IDs
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
|
|||||||
import type {
|
import type {
|
||||||
IStorage,
|
IStorage,
|
||||||
StorageStats,
|
StorageStats,
|
||||||
UpdateStatusResult,
|
UpdateStatusResult
|
||||||
LoadTasksOptions
|
|
||||||
} from '../../interfaces/storage.interface.js';
|
} from '../../interfaces/storage.interface.js';
|
||||||
import { FormatHandler } from './format-handler.js';
|
import { FormatHandler } from './format-handler.js';
|
||||||
import { FileOperations } from './file-operations.js';
|
import { FileOperations } from './file-operations.js';
|
||||||
@@ -93,30 +92,15 @@ export class FileStorage implements IStorage {
|
|||||||
* Load tasks from the single tasks.json file for a specific tag
|
* Load tasks from the single tasks.json file for a specific tag
|
||||||
* Enriches tasks with complexity data from the complexity report
|
* Enriches tasks with complexity data from the complexity report
|
||||||
*/
|
*/
|
||||||
async loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]> {
|
async loadTasks(tag?: string): Promise<Task[]> {
|
||||||
const filePath = this.pathResolver.getTasksPath();
|
const filePath = this.pathResolver.getTasksPath();
|
||||||
const resolvedTag = tag || 'master';
|
const resolvedTag = tag || 'master';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawData = await this.fileOps.readJson(filePath);
|
const rawData = await this.fileOps.readJson(filePath);
|
||||||
let tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
const tasks = this.formatHandler.extractTasks(rawData, resolvedTag);
|
||||||
|
|
||||||
// Apply filters if provided
|
|
||||||
if (options) {
|
|
||||||
// Filter by status if specified
|
|
||||||
if (options.status) {
|
|
||||||
tasks = tasks.filter((task) => task.status === options.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude subtasks if specified
|
|
||||||
if (options.excludeSubtasks) {
|
|
||||||
tasks = tasks.map((task) => ({
|
|
||||||
...task,
|
|
||||||
subtasks: []
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Enrich tasks with complexity data
|
||||||
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
|
return await this.enrichTasksWithComplexity(tasks, resolvedTag);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === 'ENOENT') {
|
if (error.code === 'ENOENT') {
|
||||||
|
|||||||
@@ -14,14 +14,7 @@ import {
|
|||||||
type StartTaskResult,
|
type StartTaskResult,
|
||||||
type ConflictCheckResult
|
type ConflictCheckResult
|
||||||
} from './services/task-execution-service.js';
|
} from './services/task-execution-service.js';
|
||||||
import {
|
|
||||||
ExportService,
|
|
||||||
type ExportTasksOptions,
|
|
||||||
type ExportResult
|
|
||||||
} from './services/export.service.js';
|
|
||||||
import { AuthManager } from './auth/auth-manager.js';
|
|
||||||
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js';
|
||||||
import type { UserContext } from './auth/types.js';
|
|
||||||
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
import type { IConfiguration } from './interfaces/configuration.interface.js';
|
||||||
import type {
|
import type {
|
||||||
Task,
|
Task,
|
||||||
@@ -54,10 +47,6 @@ export type {
|
|||||||
StartTaskResult,
|
StartTaskResult,
|
||||||
ConflictCheckResult
|
ConflictCheckResult
|
||||||
} from './services/task-execution-service.js';
|
} from './services/task-execution-service.js';
|
||||||
export type {
|
|
||||||
ExportTasksOptions,
|
|
||||||
ExportResult
|
|
||||||
} from './services/export.service.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TaskMasterCore facade class
|
* TaskMasterCore facade class
|
||||||
@@ -67,7 +56,6 @@ export class TaskMasterCore {
|
|||||||
private configManager: ConfigManager;
|
private configManager: ConfigManager;
|
||||||
private taskService: TaskService;
|
private taskService: TaskService;
|
||||||
private taskExecutionService: TaskExecutionService;
|
private taskExecutionService: TaskExecutionService;
|
||||||
private exportService: ExportService;
|
|
||||||
private executorService: ExecutorService | null = null;
|
private executorService: ExecutorService | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +80,6 @@ export class TaskMasterCore {
|
|||||||
this.configManager = null as any;
|
this.configManager = null as any;
|
||||||
this.taskService = null as any;
|
this.taskService = null as any;
|
||||||
this.taskExecutionService = null as any;
|
this.taskExecutionService = null as any;
|
||||||
this.exportService = null as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,10 +109,6 @@ export class TaskMasterCore {
|
|||||||
|
|
||||||
// Create task execution service
|
// Create task execution service
|
||||||
this.taskExecutionService = new TaskExecutionService(this.taskService);
|
this.taskExecutionService = new TaskExecutionService(this.taskService);
|
||||||
|
|
||||||
// Create export service
|
|
||||||
const authManager = AuthManager.getInstance();
|
|
||||||
this.exportService = new ExportService(this.configManager, authManager);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TaskMasterError(
|
throw new TaskMasterError(
|
||||||
'Failed to initialize TaskMasterCore',
|
'Failed to initialize TaskMasterCore',
|
||||||
@@ -259,33 +242,6 @@ export class TaskMasterCore {
|
|||||||
return this.taskExecutionService.getNextAvailableTask();
|
return this.taskExecutionService.getNextAvailableTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Export Service Methods ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export tasks to an external system (e.g., Hamster brief)
|
|
||||||
*/
|
|
||||||
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
|
|
||||||
return this.exportService.exportTasks(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export tasks from a brief ID or URL
|
|
||||||
*/
|
|
||||||
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
|
|
||||||
return this.exportService.exportFromBriefInput(briefInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate export context before prompting
|
|
||||||
*/
|
|
||||||
async validateExportContext(): Promise<{
|
|
||||||
hasOrg: boolean;
|
|
||||||
hasBrief: boolean;
|
|
||||||
context: UserContext | null;
|
|
||||||
}> {
|
|
||||||
return this.exportService.validateContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Executor Service Methods ====================
|
// ==================== Executor Service Methods ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,11 +82,10 @@ export interface Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subtask interface extending Task
|
* Subtask interface extending Task with numeric ID
|
||||||
* ID can be number (file storage) or string (API storage with display_id)
|
|
||||||
*/
|
*/
|
||||||
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
||||||
id: number | string;
|
id: number;
|
||||||
parentId: string;
|
parentId: string;
|
||||||
subtasks?: never; // Subtasks cannot have their own subtasks
|
subtasks?: never; // Subtasks cannot have their own subtasks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* Type definitions for repository operations
|
|
||||||
*/
|
|
||||||
import { Database, Tables } from './database.types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task row from database with optional joined relations
|
|
||||||
*/
|
|
||||||
export interface TaskWithRelations extends Tables<'tasks'> {
|
|
||||||
document?: {
|
|
||||||
id: string;
|
|
||||||
document_name: string;
|
|
||||||
title: string;
|
|
||||||
description: string | null;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency row with joined display_id
|
|
||||||
*/
|
|
||||||
export interface DependencyWithDisplayId {
|
|
||||||
task_id: string;
|
|
||||||
depends_on_task: {
|
|
||||||
display_id: string;
|
|
||||||
} | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task metadata structure
|
|
||||||
*/
|
|
||||||
export interface TaskMetadata {
|
|
||||||
details?: string;
|
|
||||||
testStrategy?: string;
|
|
||||||
[key: string]: unknown; // Allow additional fields but be explicit
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database update payload for tasks
|
|
||||||
*/
|
|
||||||
export type TaskDatabaseUpdate =
|
|
||||||
Database['public']['Tables']['tasks']['Update'];
|
|
||||||
/**
|
|
||||||
* Configuration for task queries
|
|
||||||
*/
|
|
||||||
export interface TaskQueryConfig {
|
|
||||||
briefId: string;
|
|
||||||
includeSubtasks?: boolean;
|
|
||||||
includeDependencies?: boolean;
|
|
||||||
includeDocument?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of a task fetch operation
|
|
||||||
*/
|
|
||||||
export interface TaskFetchResult {
|
|
||||||
task: Tables<'tasks'>;
|
|
||||||
subtasks: Tables<'tasks'>[];
|
|
||||||
dependencies: Map<string, string[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task validation errors
|
|
||||||
*/
|
|
||||||
export class TaskValidationError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public readonly field: string,
|
|
||||||
public readonly value: unknown
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'TaskValidationError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context validation errors
|
|
||||||
*/
|
|
||||||
export class ContextValidationError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'ContextValidationError';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,11 +12,17 @@ import https from 'https';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import inquirer from 'inquirer';
|
import inquirer from 'inquirer';
|
||||||
import search from '@inquirer/search';
|
import search from '@inquirer/search';
|
||||||
|
import ora from 'ora'; // Import ora
|
||||||
|
|
||||||
import { log, readJSON } from './utils.js';
|
import { log, readJSON } from './utils.js';
|
||||||
// Import command registry and utilities from @tm/cli
|
// Import new commands from @tm/cli
|
||||||
import {
|
import {
|
||||||
registerAllCommands,
|
ListTasksCommand,
|
||||||
|
ShowCommand,
|
||||||
|
AuthCommand,
|
||||||
|
ContextCommand,
|
||||||
|
StartCommand,
|
||||||
|
SetStatusCommand,
|
||||||
checkForUpdate,
|
checkForUpdate,
|
||||||
performAutoUpdate,
|
performAutoUpdate,
|
||||||
displayUpgradeNotification
|
displayUpgradeNotification
|
||||||
@@ -26,6 +32,7 @@ import {
|
|||||||
parsePRD,
|
parsePRD,
|
||||||
updateTasks,
|
updateTasks,
|
||||||
generateTaskFiles,
|
generateTaskFiles,
|
||||||
|
listTasks,
|
||||||
expandTask,
|
expandTask,
|
||||||
expandAllTasks,
|
expandAllTasks,
|
||||||
clearSubtasks,
|
clearSubtasks,
|
||||||
@@ -46,7 +53,11 @@ import {
|
|||||||
validateStrength
|
validateStrength
|
||||||
} from './task-manager.js';
|
} from './task-manager.js';
|
||||||
|
|
||||||
import { moveTasksBetweenTags } from './task-manager/move-task.js';
|
import {
|
||||||
|
moveTasksBetweenTags,
|
||||||
|
MoveTaskError,
|
||||||
|
MOVE_ERROR_CODES
|
||||||
|
} from './task-manager/move-task.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createTag,
|
createTag,
|
||||||
@@ -61,7 +72,9 @@ import {
|
|||||||
addDependency,
|
addDependency,
|
||||||
removeDependency,
|
removeDependency,
|
||||||
validateDependenciesCommand,
|
validateDependenciesCommand,
|
||||||
fixDependenciesCommand
|
fixDependenciesCommand,
|
||||||
|
DependencyError,
|
||||||
|
DEPENDENCY_ERROR_CODES
|
||||||
} from './dependency-manager.js';
|
} from './dependency-manager.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -90,6 +103,7 @@ import {
|
|||||||
displayBanner,
|
displayBanner,
|
||||||
displayHelp,
|
displayHelp,
|
||||||
displayNextTask,
|
displayNextTask,
|
||||||
|
displayTaskById,
|
||||||
displayComplexityReport,
|
displayComplexityReport,
|
||||||
getStatusWithColor,
|
getStatusWithColor,
|
||||||
confirmTaskOverwrite,
|
confirmTaskOverwrite,
|
||||||
@@ -98,6 +112,8 @@ import {
|
|||||||
displayModelConfiguration,
|
displayModelConfiguration,
|
||||||
displayAvailableModels,
|
displayAvailableModels,
|
||||||
displayApiKeyStatus,
|
displayApiKeyStatus,
|
||||||
|
displayAiUsageSummary,
|
||||||
|
displayMultipleTasksSummary,
|
||||||
displayTaggedTasksFYI,
|
displayTaggedTasksFYI,
|
||||||
displayCurrentTagIndicator,
|
displayCurrentTagIndicator,
|
||||||
displayCrossTagDependencyError,
|
displayCrossTagDependencyError,
|
||||||
@@ -121,6 +137,10 @@ import {
|
|||||||
setModel,
|
setModel,
|
||||||
getApiKeyStatusReport
|
getApiKeyStatusReport
|
||||||
} from './task-manager/models.js';
|
} from './task-manager/models.js';
|
||||||
|
import {
|
||||||
|
isValidTaskStatus,
|
||||||
|
TASK_STATUS_OPTIONS
|
||||||
|
} from '../../src/constants/task-status.js';
|
||||||
import {
|
import {
|
||||||
isValidRulesAction,
|
isValidRulesAction,
|
||||||
RULES_ACTIONS,
|
RULES_ACTIONS,
|
||||||
@@ -1667,12 +1687,29 @@ function registerCommands(programInstance) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========================================
|
// Register the set-status command from @tm/cli
|
||||||
// Register All Commands from @tm/cli
|
// Handles task status updates with proper error handling and validation
|
||||||
// ========================================
|
SetStatusCommand.registerOn(programInstance);
|
||||||
// Use the centralized command registry to register all CLI commands
|
|
||||||
// This replaces individual command registrations and reduces duplication
|
// NEW: Register the new list command from @tm/cli
|
||||||
registerAllCommands(programInstance);
|
// This command handles all its own configuration and logic
|
||||||
|
ListTasksCommand.registerOn(programInstance);
|
||||||
|
|
||||||
|
// Register the auth command from @tm/cli
|
||||||
|
// Handles authentication with tryhamster.com
|
||||||
|
AuthCommand.registerOn(programInstance);
|
||||||
|
|
||||||
|
// Register the context command from @tm/cli
|
||||||
|
// Manages workspace context (org/brief selection)
|
||||||
|
ContextCommand.registerOn(programInstance);
|
||||||
|
|
||||||
|
// Register the show command from @tm/cli
|
||||||
|
// Displays detailed information about tasks
|
||||||
|
ShowCommand.registerOn(programInstance);
|
||||||
|
|
||||||
|
// Register the start command from @tm/cli
|
||||||
|
// Starts working on a task by launching claude-code with a standardized prompt
|
||||||
|
StartCommand.registerOn(programInstance);
|
||||||
|
|
||||||
// expand command
|
// expand command
|
||||||
programInstance
|
programInstance
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js';
|
||||||
import { isTaskDependentOn } from '../task-manager.js';
|
import { isTaskDependentOn } from '../task-manager.js';
|
||||||
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a subtask to a parent task
|
* Add a subtask to a parent task
|
||||||
@@ -139,7 +142,11 @@ async function addSubtask(
|
|||||||
// Write the updated tasks back to the file with proper context
|
// Write the updated tasks back to the file with proper context
|
||||||
writeJSON(tasksPath, data, projectRoot, tag);
|
writeJSON(tasksPath, data, projectRoot, tag);
|
||||||
|
|
||||||
// Note: Task file generation is no longer supported and has been removed
|
// Generate task files if requested
|
||||||
|
if (generateFiles) {
|
||||||
|
log('info', 'Regenerating task files...');
|
||||||
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||||
|
}
|
||||||
|
|
||||||
return newSubtask;
|
return newSubtask;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
setTasksForTag,
|
setTasksForTag,
|
||||||
traverseDependencies
|
traverseDependencies
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
import {
|
import {
|
||||||
findCrossTagDependencies,
|
findCrossTagDependencies,
|
||||||
getDependentTaskIds,
|
getDependentTaskIds,
|
||||||
@@ -141,7 +142,13 @@ async function moveTask(
|
|||||||
results.push(result);
|
results.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Task file generation is no longer supported and has been removed
|
// Generate files once at the end if requested
|
||||||
|
if (generateFiles) {
|
||||||
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||||
|
tag: tag,
|
||||||
|
projectRoot: projectRoot
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
message: `Successfully moved ${sourceIds.length} tasks/subtasks`,
|
||||||
@@ -202,7 +209,12 @@ async function moveTask(
|
|||||||
// The writeJSON function will filter out _rawTaggedData automatically
|
// The writeJSON function will filter out _rawTaggedData automatically
|
||||||
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
writeJSON(tasksPath, rawData, options.projectRoot, tag);
|
||||||
|
|
||||||
// Note: Task file generation is no longer supported and has been removed
|
if (generateFiles) {
|
||||||
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
|
||||||
|
tag: tag,
|
||||||
|
projectRoot: projectRoot
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import path from 'path';
|
||||||
import { log, readJSON, writeJSON } from '../utils.js';
|
import { log, readJSON, writeJSON } from '../utils.js';
|
||||||
|
import generateTaskFiles from './generate-task-files.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a subtask from its parent task
|
* Remove a subtask from its parent task
|
||||||
@@ -106,7 +108,11 @@ async function removeSubtask(
|
|||||||
// Write the updated tasks back to the file with proper context
|
// Write the updated tasks back to the file with proper context
|
||||||
writeJSON(tasksPath, data, projectRoot, tag);
|
writeJSON(tasksPath, data, projectRoot, tag);
|
||||||
|
|
||||||
// Note: Task file generation is no longer supported and has been removed
|
// Generate task files if requested
|
||||||
|
if (generateFiles) {
|
||||||
|
log('info', 'Regenerating task files...');
|
||||||
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath), context);
|
||||||
|
}
|
||||||
|
|
||||||
return convertedTask;
|
return convertedTask;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ describe('addSubtask function', () => {
|
|||||||
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
|
||||||
expect(parentTask.subtasks).toHaveLength(1);
|
expect(parentTask.subtasks).toHaveLength(1);
|
||||||
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
expect(parentTask.subtasks[0].title).toBe('New Subtask');
|
||||||
|
expect(mockGenerateTaskFiles).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should convert an existing task to a subtask', async () => {
|
test('should convert an existing task to a subtask', async () => {
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ describe('moveTask (unit)', () => {
|
|||||||
).rejects.toThrow(/Number of source IDs/);
|
).rejects.toThrow(/Number of source IDs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('batch move calls generateTaskFiles once when flag true', async () => {
|
||||||
|
await moveTask('tasks.json', '1,2', '3,4', true, { tag: 'master' });
|
||||||
|
expect(generateTaskFiles).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('error when tag invalid', async () => {
|
test('error when tag invalid', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
moveTask('tasks.json', '1', '2', false, { tag: 'ghost' })
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { defineConfig } from 'tsdown';
|
import { defineConfig } from 'tsdown';
|
||||||
import { baseConfig, mergeConfig } from '@tm/build-config';
|
import { baseConfig, mergeConfig } from '@tm/build-config';
|
||||||
import { config } from 'dotenv';
|
import 'dotenv/config';
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// Load .env file explicitly with absolute path
|
|
||||||
config({ path: resolve(process.cwd(), '.env') });
|
|
||||||
|
|
||||||
// Get all TM_PUBLIC_* env variables for build-time injection
|
// Get all TM_PUBLIC_* env variables for build-time injection
|
||||||
const getBuildTimeEnvs = () => {
|
const getBuildTimeEnvs = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user