openai-compatible-task-master
Version:
使用MCP解析PRD文档并生成任务列表
810 lines • 40.5 kB
JavaScript
import { Command } from 'commander';
import inquirer from 'inquirer';
import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import dotenv from 'dotenv';
import { parsePRD } from './services/parse_prd.js';
import { updateTasks } from './services/update_tasks.js';
import { listTasks } from './services/list_tasks.js';
import { setTaskStatus } from './services/set_status.js';
import { readTask } from './services/read_task.js';
import { breakupTask } from './services/breakup_task.js';
import { copyTemplateFile, copyTemplateDirectory } from './utils/file_utils.js';
import { boomerangOctmMode } from './config/boomerang-octm-mode.js';
// 获取当前文件的目录路径(在ES模块中使用)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 获取npm包根目录(假设当前文件在build子目录中)
const packageRoot = path.resolve(__dirname, '..');
// 加载.env文件
dotenv.config({ path: '.env.octm' });
const logLevels = {
error: 0,
warn: 1,
info: 2,
debug: 3
};
// 存储当前日志级别数字
let currentLogLevelNumber = logLevels.info;
// 导出函数以获取当前日志级别数字
export function getCurrentLogLevelNumber() {
return currentLogLevelNumber;
}
function setLogLevel(level) {
const normalizedLevel = level.toLowerCase();
const targetLevel = logLevels[normalizedLevel] ?? logLevels.info;
currentLogLevelNumber = targetLevel; // 更新当前级别
// 重写 console 方法
const originalConsole = { ...console };
console.debug = (...args) => {
if (targetLevel >= logLevels.debug)
originalConsole.debug(...args);
};
console.info = (...args) => {
if (targetLevel >= logLevels.info)
originalConsole.info(...args);
};
console.warn = (...args) => {
if (targetLevel >= logLevels.warn)
originalConsole.warn(...args);
};
console.error = (...args) => {
if (targetLevel >= logLevels.error)
originalConsole.error(...args);
};
}
// 从环境变量获取日志级别,默认为 info
const defaultLogLevel = (process.env.LOG_LEVEL || 'info').toLowerCase();
if (!Object.keys(logLevels).includes(defaultLogLevel)) {
console.warn(chalk.yellow(`警告: 无效的日志级别 "${defaultLogLevel}",将使用默认值 "info"`));
setLogLevel('info');
}
else {
setLogLevel(defaultLogLevel);
}
const program = new Command();
program
.name('octm-cli')
.description('初始化项目规则')
.version('1.6.0')
.option('--log-level <level>', '日志级别 (error/warn/info/debug)', defaultLogLevel);
// 处理全局选项
program.hook('preAction', (thisCommand) => {
const options = thisCommand.opts();
if (options.logLevel) {
const level = options.logLevel.toLowerCase();
if (Object.keys(logLevels).includes(level)) {
setLogLevel(level);
}
else {
console.warn(chalk.yellow(`警告: 无效的日志级别 "${level}",将使用默认值 "${defaultLogLevel}"`));
}
}
});
// 创建boomerang-octm模式的JSON对象
function createBoomerangOctmMode() {
return boomerangOctmMode;
}
// 更新或创建.roomodes文件
function updateRoomodesFile(targetRoot) {
const roomodesPath = path.join(targetRoot, '.roomodes');
let existingContent = { customModes: [] };
// 检查.roomodes文件是否存在
if (fs.existsSync(roomodesPath)) {
try {
// 读取现有文件
const fileContent = fs.readFileSync(roomodesPath, 'utf8');
existingContent = JSON.parse(fileContent);
console.log(chalk.blue('找到现有的.roomodes文件,将进行替换'));
}
catch {
console.warn(chalk.yellow('⚠️ 无法解析现有的.roomodes文件,将创建新文件'));
}
}
else {
console.log(chalk.blue('📝 未找到.roomodes文件,将创建新文件'));
}
// 检查是否已经有boomerang-octm模式
const hasBoomerang = existingContent.customModes.some((mode) => mode.name === 'boomerang-octm');
if (hasBoomerang) {
// 如果已存在,移除现有的boomerang-octm模式
existingContent.customModes = existingContent.customModes.filter((mode) => mode.name !== 'boomerang-octm');
console.log(chalk.blue('📝 找到现有的boomerang-octm模式,将进行替换'));
}
// 添加boomerang-octm模式
existingContent.customModes.push(createBoomerangOctmMode());
// 写入更新后的文件
fs.writeFileSync(roomodesPath, JSON.stringify(existingContent, null, 2));
console.log(chalk.green('✅ 已成功添加/更新boomerang-octm模式到.roomodes文件'));
}
// 检查Node.js版本是否满足要求
function checkNodeVersion() {
const requiredVersion = '18.0.0';
const currentVersion = process.version.slice(1); // 移除版本号前的'v'
const current = currentVersion.split('.').map(Number);
const required = requiredVersion.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (current[i] > required[i])
return true;
if (current[i] < required[i])
return false;
}
return true;
}
program
.command('init')
.description('初始化项目配置')
.action(async () => {
// 检查Node.js版本
if (!checkNodeVersion()) {
console.error(chalk.bold.red('错误: ') + chalk.red(`当前Node.js版本 ${process.version} 不满足要求。`));
console.error(chalk.white('本工具需要 ') + chalk.cyan('Node.js >= 18.0.0'));
console.log(chalk.yellow('\n解决方案:'));
console.log(chalk.white('1. 使用 nvm 安装并切换到合适的Node.js版本:'));
console.log(chalk.cyan(' nvm install 18'));
console.log(chalk.cyan(' nvm use 18'));
console.log(chalk.white('2. 或直接从Node.js官网下载安装最新LTS版本:'));
console.log(chalk.cyan(' https://nodejs.org/'));
process.exit(1);
}
console.log(chalk.cyan('欢迎使用 ') + chalk.bold.green('openai-compatible-task-master') + chalk.cyan(' 初始化工具!'));
// 首先选择使用方式
const { useMode } = await inquirer.prompt([
{
type: 'list',
name: 'useMode',
message: chalk.yellow('请选择使用方式:'),
choices: [
/*{
name: 'MCP (推荐) - 作为大模型的工具使用,支持更智能的交互',
value: 'mcp'
},*/
{
name: 'CLI - 作为命令行工具使用,支持脚本和自动化',
value: 'cli'
}
]
}
]);
const { ruleType } = await inquirer.prompt([
{
type: 'list',
name: 'ruleType',
message: chalk.yellow('请选择规则类型:'),
choices: ['roo code', 'cursor', 'cline']
}
]);
console.log(chalk.blue('用户选择了: ') + chalk.bold.green(`${useMode} 模式,${ruleType} 规则`));
// 在用户完成选择后,检查并更新 git 配置
const isGitRepo = fs.existsSync(path.join(process.cwd(), '.git'));
if (isGitRepo) {
console.log(chalk.blue('\n📦 检测到当前是 Git 仓库'));
// 读取现有的 .gitignore 文件内容
const gitignorePath = path.join(process.cwd(), '.gitignore');
let existingContent = '';
if (fs.existsSync(gitignorePath)) {
existingContent = fs.readFileSync(gitignorePath, 'utf8');
}
// 需要添加的内容
const newEntries = [
'',
'# OCTM specific',
'.env.octm',
'tasks/',
'examples/',
''
].filter(entry => !existingContent.includes(entry));
if (newEntries.length > 0) {
// 添加新内容到 .gitignore
const newContent = existingContent +
(existingContent && !existingContent.endsWith('\n') ? '\n' : '') +
newEntries.join('\n');
fs.writeFileSync(gitignorePath, newContent);
console.log(chalk.yellow('\n⚠️ 注意:已更新 .gitignore 文件!'));
console.log(chalk.white('新增了以下内容:'));
newEntries.forEach(entry => {
if (entry && !entry.startsWith('#')) {
console.log(chalk.cyan(` ${entry}`));
}
});
console.log(chalk.red('\n❗ 请检查这些忽略规则是否会影响到您的项目文件!'));
console.log(chalk.gray('如有需要,请编辑 .gitignore 文件进行调整。'));
}
}
// 定义源目录和目标目录的映射
const pathMappings = {
'cursor': {
source: 'rules/.cursor',
target: '.cursor/rules'
},
'cline': {
source: 'rules/.clinerules',
target: '.clinerules'
},
'roo code': {
source: 'rules/.roo_code',
target: '.roo/rules'
}
};
// 获取映射配置
const mapping = pathMappings[ruleType];
if (!mapping) {
throw new Error(`未知的规则类型: ${ruleType}`);
}
// 项目根目录是用户当前工作目录
const targetRoot = process.cwd();
console.log(chalk.blue('项目根目录: ') + chalk.bold.green(targetRoot));
// 模板根目录是npm包所在位置
const templateRoot = packageRoot;
try {
// 如果用户选择了"roo code",更新.roomodes文件
if (ruleType === 'roo code') {
updateRoomodesFile(targetRoot);
// 更新.rooignore文件
const rooignorePath = path.join(targetRoot, '.rooignore');
let existingContent = '';
if (fs.existsSync(rooignorePath)) {
existingContent = fs.readFileSync(rooignorePath, 'utf8');
}
const newEntries = [
'.env.octm',
'tasks/',
'examples/',
''
].filter(entry => !existingContent.includes(entry));
if (newEntries.length > 0) {
fs.writeFileSync(rooignorePath, newEntries.join('\n'));
}
}
// 根据使用方式复制相应的目录
const sourceSubDir = useMode === 'mcp' ? 'mcp' : 'cli';
const sourceDir = path.join(mapping.source, sourceSubDir);
const targetDir = mapping.target;
// 复制选定的目录
copyTemplateDirectory(templateRoot, targetRoot, sourceDir, targetDir);
// copy .env.example
copyTemplateFile(templateRoot, targetRoot, '.env.octm.example', '.env.octm');
copyTemplateFile(templateRoot, targetRoot, '.env.octm.example', '.env.octm.example');
// copy examples directory
copyTemplateDirectory(templateRoot, targetRoot, 'examples', 'examples');
// 复制最佳实践指南到对应目录
let bestPracticeSource;
let bestPracticeTarget;
// 根据规则类型和使用模式确定源文件和目标文件
if (ruleType === 'cursor') {
// 对于cursor规则类型,使用.mdc扩展名,从.cursor目录复制
bestPracticeSource = 'rules/.cursor/octm-best-practice.mdc';
bestPracticeTarget = path.join(mapping.target, 'octm-best-practice.mdc');
}
else if (ruleType === 'cline') {
// 从.clinerules目录复制
bestPracticeSource = 'rules/.clinerules/octm-best-practice.md';
bestPracticeTarget = path.join(mapping.target, 'octm-best-practice.md');
}
else { // roo code
// 从.roo_code目录复制
bestPracticeSource = 'rules/.roo_code/octm-best-practice.md';
bestPracticeTarget = path.join(mapping.target, 'octm-best-practice.md');
}
// 复制最佳实践文件
copyTemplateFile(templateRoot, targetRoot, bestPracticeSource, bestPracticeTarget);
// 如果是 roo code,额外复制 boomerang 指南
if (ruleType === 'roo code') {
const boomerangSource = 'rules/.roo_code/rules-boomerang.md';
const boomerangTarget = '.roo/rules-boomerang-mode/rules.md';
copyTemplateFile(templateRoot, targetRoot, boomerangSource, boomerangTarget);
}
console.log(chalk.bold.green('\n初始化完成!'));
// 打印特定于 ruleType 的说明
if (ruleType === 'roo code') {
console.log(chalk.cyan('\n🚀 进一步配置 (Roo Code): ') + chalk.white('为了获得最佳体验,强烈建议您配置 Roo Code 的 Boomerang 模式。请参考文档:') + chalk.underline.cyan('https://docs.roocode.com/features/boomerang-tasks'));
// 根据用户选择的模式提供不同的权限建议
if (useMode === 'mcp') {
console.log(chalk.yellow('⚠️ 重要: ') + chalk.white('请确保在 Roo Code 设置中为 Boomerang 模式启用 ' + chalk.bold('MCP执行权限') + ', 因为它需要调用本工具的 MCP 服务。'));
}
else { // useMode === 'cli'
console.log(chalk.yellow('⚠️ 重要: ') + chalk.white('请确保在 Roo Code 设置中为 Boomerang 模式启用 ' + chalk.bold('命令行执行权限') + ', 因为它需要通过命令行调用本工具。'));
}
console.log(chalk.white('Boomerang 模式本身通常不直接执行命令,而是委托给配置好的工具(如本工具)。'));
}
// 打印配置说明 (基于 useMode)
if (useMode === 'mcp') {
console.log(chalk.cyan('\n📘 MCP 模式配置说明:'));
console.log(chalk.white('在 Claude Desktop 中添加以下配置:'));
console.log(chalk.gray('{'));
console.log(chalk.gray(' "mcpServers": {'));
console.log(chalk.gray(' "task-parser": {'));
console.log(chalk.gray(' "command": "npx",'));
console.log(chalk.gray(' "args": ["octm"],'));
console.log(chalk.gray(' "env": {'));
console.log(chalk.gray(' "OPENAI_API_KEY": "你的API密钥",'));
console.log(chalk.gray(' "OPENAI_API_URL": "https://api.openai.com/v1",'));
console.log(chalk.gray(' "OPENAI_MODEL": "gpt-4",'));
console.log(chalk.gray(' "STREAM_MODE": "true", // 有些模型必须设置为true,比如 qwq-plus'));
console.log(chalk.gray(' "LOG_LEVEL": "info" // 可选值: error/warn/info/debug'));
console.log(chalk.gray(' }'));
console.log(chalk.gray(' }'));
console.log(chalk.gray(' }'));
console.log(chalk.gray('}'));
}
else {
console.log(chalk.cyan('\n📘 CLI 模式配置说明:'));
console.log(chalk.white('在 .env.octm 文件中配置以下内容:'));
console.log(chalk.gray('OPENAI_API_KEY=your_api_key_here'));
console.log(chalk.gray('OPENAI_API_URL=https://api.openai.com/v1'));
console.log(chalk.gray('OPENAI_MODEL=gpt-4'));
console.log(chalk.gray('INPUT_PATH=./examples/prd-example.md'));
console.log(chalk.gray('TASKS_PATH=/tasks/tasks.json'));
console.log(chalk.gray('NUM_TASKS=5'));
console.log(chalk.gray('STREAM_MODE=true'));
}
console.log(chalk.cyan('\n📖 详细文档请查看: ') + chalk.underline.cyan(path.join(mapping.target, 'usage.md')));
// 添加第一次使用的示例指引
console.log(chalk.blue('\n🚀 立即开始使用:'));
console.log(chalk.white('运行以下命令解析示例文件:'));
console.log(chalk.cyan(' npx octm-cli parse-files --input examples/prd-example.md --output tasks/tasks.json --tasks 5'));
console.log(chalk.gray(' 这将会:'));
console.log(chalk.gray(' 1. 解析示例文件'));
console.log(chalk.gray(' 2. 生成5个任务'));
console.log(chalk.gray(' 3. 保存到tasks/tasks.json'));
}
catch (error) {
console.error(chalk.bold.red('错误: ') + chalk.red(error.message));
process.exit(1);
}
});
program
.command('parse-files')
.description('解析文件并生成任务')
.requiredOption('--input <paths>', '输入文件路径,多个文件用"|"分隔')
.option('--output <path>', '输出任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.option('--tasks <number>', '要生成的任务数量', (value) => parseInt(value), process.env.NUM_TASKS ? parseInt(process.env.NUM_TASKS) : 5)
.option('--additional-prompts <text>', '额外的提示信息,将优先于其他冲突的指令')
.action(async (options) => {
console.log(chalk.cyan('🚀 开始解析文件...'));
// 从环境变量获取配置
const openaiUrl = process.env.OPENAI_API_URL;
const apiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL;
const streamMode = process.env.STREAM_MODE === 'true';
// 验证必要的环境变量
if (!openaiUrl) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 OpenAI 兼容 URL。请在.env.octm文件中设置 OPENAI_API_URL。'));
process.exit(1);
}
if (!apiKey) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 API 密钥。请在.env.octm文件中设置 OPENAI_API_KEY。'));
process.exit(1);
}
if (!model) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供模型名称。请在.env.octm文件中设置 OPENAI_MODEL。'));
process.exit(1);
}
// 解析输入文件路径并打印
const filePaths = options.input.split('|').map((p) => p.trim());
console.log(chalk.blue('📝 输入文件:'));
filePaths.forEach((p) => console.log(' ' + chalk.white(p)));
try {
// 获取当前工作目录
const projectDir = process.cwd();
// 直接调用parsePRD函数,该函数已经支持多文件处理
console.log(chalk.blue('🧠 正在分析文件内容...'));
const result = await parsePRD(projectDir, options.input, options.output, options.tasks, openaiUrl, apiKey, model, streamMode, options.additionalPrompts);
console.log(chalk.bold.green('\n✅ 解析完成!'));
console.log(chalk.white(`共生成 ${result.tasks.length} 个任务,已保存到: ${options.output}`));
// 打印任务列表预览
console.log(chalk.blue('\n📋 任务列表预览:'));
result.tasks.forEach(task => {
console.log(chalk.green(`[${task.id}] `) + chalk.white(task.title) + chalk.gray(` (优先级: ${task.priority})`));
});
}
catch (error) {
console.error(chalk.bold.red('❌ 错误: ') + chalk.red(error.message));
process.exit(1);
}
});
program
.command('update-tasks')
.description('更新现有任务列表')
.option('--tasks-path <path>', '任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.requiredOption('--prompt <text>', '用于更新任务的提示内容')
.option('--from-id <id>', '起始任务ID', (value) => {
// 保留原始值,不尝试将子任务ID(如"1.1")转换为数字
return value;
}, '1')
.action(async (options) => {
console.log(chalk.cyan('🔄 开始更新任务...'));
// 从环境变量获取配置
const openaiUrl = process.env.OPENAI_API_URL;
const apiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL;
const streamMode = process.env.STREAM_MODE === 'true';
// 验证必要的环境变量
if (!openaiUrl) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 OpenAI 兼容 URL。请在.env.octm文件中设置 OPENAI_API_URL。'));
process.exit(1);
}
if (!apiKey) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 API 密钥。请在.env.octm文件中设置 OPENAI_API_KEY。'));
process.exit(1);
}
if (!model) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供模型名称。请在.env.octm文件中设置 OPENAI_MODEL。'));
process.exit(1);
}
try {
// 检查任务文件是否存在
const projectDir = process.cwd();
const fullTasksPath = path.resolve(projectDir, options.tasksPath);
if (!fs.existsSync(fullTasksPath)) {
throw new Error(`任务文件不存在: ${fullTasksPath}`);
}
console.log(chalk.blue('📋 任务文件: ') + chalk.white(options.tasksPath));
console.log(chalk.blue('🔍 起始任务ID: ') + chalk.white(options.fromId));
console.log(chalk.blue('💡 更新提示: ') + chalk.white(options.prompt));
// 调用updateTasks函数
console.log(chalk.blue('🧠 正在更新任务...'));
const result = await updateTasks(projectDir, options.tasksPath, options.prompt, options.fromId, openaiUrl, apiKey, model, streamMode);
if (result.saved) {
console.log(chalk.bold.green('\n✅ 任务更新完成!'));
console.log(chalk.white(`任务已保存到: ${options.tasksPath}`));
// 显示已更新的任务
try {
const updatedTasksData = JSON.parse(fs.readFileSync(fullTasksPath, 'utf8'));
let tasksToShow = [];
// 检查是否为子任务ID
if (String(options.fromId).includes('.')) {
const [parentId, subTaskNumStr] = String(options.fromId).split('.');
// 只显示更新的子任务
updatedTasksData.tasks.forEach(task => {
if (task.id === parentId && task.subTasks) {
// 找到父任务,筛选出子任务
const matchingSubTasks = task.subTasks.filter(subTask => {
if (!subTask.id.includes('.'))
return false;
const [taskParentId, taskSubNumStr] = subTask.id.split('.');
return taskParentId === parentId && taskSubNumStr >= subTaskNumStr;
});
tasksToShow = [...tasksToShow, ...matchingSubTasks];
}
});
}
else {
// 显示主任务
tasksToShow = updatedTasksData.tasks.filter((task) => {
// 检查任务ID是否大于等于fromId
return String(task.id) >= String(options.fromId);
});
}
console.log(chalk.blue('\n📋 更新后的任务:'));
tasksToShow.forEach((task) => {
console.log(chalk.green(`[${task.id}] `) + chalk.white(task.title) + chalk.gray(` (优先级: ${task.priority}, 状态: ${task.status})`));
});
}
catch (error) {
console.log(chalk.yellow('无法显示更新后的任务预览'));
console.error(error);
}
}
else {
console.log(chalk.yellow('\n⚠️ 没有找到需要更新的任务'));
}
}
catch (error) {
handleTaskFileError(error, 'update-tasks', options);
}
});
/**
* 处理任务文件相关错误,并根据命令类型提供相应的帮助信息
* @param error 错误对象
* @param commandName 命令名称
* @param options 命令选项
*/
function handleTaskFileError(error, commandName, options) {
const errorMessage = error.message;
console.error(chalk.bold.red('❌ 错误: ') + chalk.red(errorMessage));
// 检查错误是否与任务文件不存在有关
if (errorMessage.includes('任务文件不存在') || errorMessage.includes('找不到文件')) {
console.log(chalk.yellow('\n可能的解决方案:'));
console.log(chalk.white('1. 使用 --tasks-path 参数指定正确的任务文件路径:'));
// 根据不同命令构建建议命令行
let suggestedCommand = `npx octm-cli ${commandName}`;
// 添加命令特定的必要参数
switch (commandName) {
case 'read-task':
suggestedCommand += ` --task-id ${options.taskId}`;
break;
case 'set-status':
suggestedCommand += ` --task-id ${options.taskId} --status ${options.status}`;
break;
case 'breakup-task':
suggestedCommand += ` --task-id ${options.taskId}`;
if (options.prompt) {
suggestedCommand += ` --prompt "${options.prompt}"`;
}
break;
}
// 添加tasks-path参数
suggestedCommand += ' --tasks-path 你的任务文件路径.json';
console.log(chalk.cyan(` ${suggestedCommand}`));
console.log(chalk.white('2. 或者先使用 parse-files 命令生成任务文件:'));
console.log(chalk.cyan(' npx octm-cli parse-files --input 你的文件.md'));
}
process.exit(1);
}
program
.command('list-tasks')
.description('列出所有任务并提示下一个未完成的任务')
.option('--tasks-path <path>', '任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.option('-d, --detail', '显示所有未完成任务的详细信息')
.action(async (options) => {
console.log(chalk.cyan('📋 开始列出任务...'));
try {
const projectDir = process.cwd();
const { tasksData, nextPendingTask, nextPendingSubtaskPath, unfinishedTaskDetails } = await listTasks(projectDir, options.tasksPath, options.detail);
// 显示项目信息
console.log(chalk.blue('\n📊 项目信息:'));
console.log(chalk.white(`项目名称: ${tasksData.metadata.projectName}`));
console.log(chalk.white(`总任务数: ${tasksData.metadata.totalTasks}`));
console.log(chalk.white(`源文件: ${tasksData.metadata.sourceFile}`));
console.log(chalk.white(`生成时间: ${tasksData.metadata.generatedAt}`));
// 显示所有任务的简要信息
console.log(chalk.blue('\n📋 任务列表:'));
// 递归显示任务及其子任务
function displayTask(task, indent = '', currentDepth = 1) {
const statusColor = task.status === 'done' ? chalk.green :
task.status === 'in-progress' ? chalk.yellow :
chalk.gray;
const taskLine = indent +
chalk.cyan(`[${task.id}] `) +
chalk.white(task.title) +
chalk.gray(` (优先级: ${task.priority}) `) +
statusColor(`[${task.status}]`);
if (task.subTasks && task.subTasks.length > 0) {
console.log(taskLine + chalk.gray(` (${task.subTasks.length} 个子任务)`));
// 如果未指定最大深度或未达到最大深度,则显示子任务
task.subTasks.forEach(subTask => {
displayTask(subTask, indent + ' ', currentDepth + 1);
});
}
else {
console.log(taskLine);
}
}
// 显示所有主任务及其子任务
tasksData.tasks.forEach(task => displayTask(task));
// 如果启用了详细信息模式,显示所有未完成任务的详细信息
if (options.detail && unfinishedTaskDetails && unfinishedTaskDetails.length > 0) {
console.log(chalk.blue('\n📝 未完成任务详情:'));
unfinishedTaskDetails.forEach(task => {
console.log(chalk.cyan(`\n[${task.id}] `) + chalk.bold.white(task.title));
console.log(chalk.gray('路径: ') + chalk.white(task.path));
console.log(chalk.gray('状态: ') + chalk.yellow(task.status));
console.log(chalk.gray('优先级: ') + chalk.white(task.priority));
console.log(chalk.gray('详细信息:'));
console.log(chalk.white(task.details));
console.log(chalk.gray('---'));
});
}
// 显示下一个未完成的主任务
if (nextPendingTask) {
console.log(chalk.blue('\n⏭️ 下一个待处理的任务:'));
console.log(chalk.cyan(`[${nextPendingTask.id}] `) + chalk.white(nextPendingTask.title));
console.log(chalk.gray('描述: ') + chalk.white(nextPendingTask.description));
console.log(chalk.gray('优先级: ') + chalk.white(nextPendingTask.priority));
console.log(chalk.gray('状态: ') + chalk.white(nextPendingTask.status));
if (nextPendingTask.dependencies && nextPendingTask.dependencies.length > 0) {
console.log(chalk.gray('依赖任务: ') + chalk.white(nextPendingTask.dependencies.join(', ')));
}
// 如果下一个任务有子任务,显示提示
if (nextPendingTask.subTasks && nextPendingTask.subTasks.length > 0) {
console.log(chalk.gray('子任务数量: ') + chalk.white(nextPendingTask.subTasks.length.toString()));
console.log(chalk.gray('提示: 使用 read-task 命令查看子任务详情'));
}
}
else {
console.log(chalk.green('\n🎉 恭喜!所有任务都已完成!'));
}
// 如果找到了未完成的子任务路径
if (nextPendingSubtaskPath.length > 0) {
const lastTask = nextPendingSubtaskPath[nextPendingSubtaskPath.length - 1];
console.log(chalk.blue('\n🔍 最优先完成的任务路径:'));
// 显示路径中的所有任务
nextPendingSubtaskPath.forEach((task, index) => {
const indent = ' '.repeat(index);
const isLastTask = index === nextPendingSubtaskPath.length - 1;
const statusColor = task.status === 'done' ? chalk.green :
task.status === 'in-progress' ? chalk.yellow :
chalk.gray;
// 最后一个任务(目标子任务)用不同的标记突出显示
const prefix = isLastTask ?
chalk.magenta('➤ ') :
chalk.blue('└─ ');
console.log(indent + prefix +
chalk.cyan(`[${task.id}] `) +
(isLastTask ? chalk.bold.white(task.title) : chalk.white(task.title)) +
chalk.gray(` (优先级: ${task.priority}) `) +
statusColor(`[${task.status}]`));
});
// 显示建议消息
console.log(chalk.magenta('\n💡 建议: ') + chalk.bold.white(`你应该首先完成 ${lastTask.id} 子任务`));
console.log(chalk.gray('描述: ') + chalk.white(lastTask.description));
console.log(chalk.gray('使用以下命令查看详情: ') +
chalk.cyan(`npx octm-cli read-task --task-id ${lastTask.id}`));
}
}
catch (error) {
handleTaskFileError(error, 'list-tasks', options);
}
});
program
.command('set-status')
.description('更新任务状态')
.option('--tasks-path <path>', '任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.requiredOption('--task-id <id>', '要更新状态的任务ID')
.requiredOption('--status <status>', '新的任务状态 (pending/in-progress/done)')
.option('--summary <text>', '当状态为done时的任务完成总结(状态为done时必填)。总结应包含已实现的功能、解决的问题、实现细节和注意事项')
.action(async (options) => {
console.log(chalk.cyan('🔄 开始更新任务状态...'));
try {
// 验证状态值
const status = options.status.toLowerCase();
if (!['pending', 'in-progress', 'done'].includes(status)) {
throw new Error('无效的状态值。必须是 pending、in-progress 或 done 之一');
}
// 验证总结参数,在状态为done时必须提供
if (status === 'done' && !options.summary) {
console.error(chalk.bold.red('错误: ') + chalk.red('当设置状态为done时,必须使用--summary参数提供完成总结'));
console.log(chalk.yellow('\n📝 如何写好任务完成总结:'));
console.log(chalk.white(' 1. 简明扼要地描述已实现的功能和解决的问题'));
console.log(chalk.white(' 2. 提及重要的实现细节和采用的技术方案'));
console.log(chalk.white(' 3. 指出任何潜在的限制或需要注意的事项'));
console.log(chalk.white(' 4. 如有适用,提及后续可能的优化方向'));
console.log(chalk.white('\n示例: --summary "实现了用户认证功能,包括登录、注册和密码重置。采用JWT进行身份验证,使用bcrypt加密密码。添加了必要的输入验证和错误处理。"'));
console.log(chalk.white('\n命令示例:'));
console.log(chalk.cyan(` npx octm-cli set-status --task-id ${options.taskId} --status done --summary "实现了任务要求的功能,包括...,解决了...的问题"`));
process.exit(1);
}
// 验证总结参数,只有在状态为done时才可以提供
if (options.summary && status !== 'done') {
console.warn(chalk.yellow('⚠️ 警告: --summary 参数只有在状态为 done 时才有效,将被忽略'));
}
const projectDir = process.cwd();
console.log(chalk.blue('📋 任务文件: ') + chalk.white(options.tasksPath));
console.log(chalk.blue('🎯 任务ID: ') + chalk.white(options.taskId));
console.log(chalk.blue('📝 新状态: ') + chalk.white(status));
if (options.summary && status === 'done') {
console.log(chalk.blue('📑 完成总结: ') + chalk.white(options.summary));
}
const result = await setTaskStatus(projectDir, options.tasksPath, String(options.taskId), // 确保 taskId 是字符串
status, options.summary // 传递总结参数
);
if (result.saved) {
console.log(chalk.bold.green('\n✅ ' + result.message));
}
else {
console.log(chalk.yellow('\n⚠️ ' + result.message));
}
}
catch (error) {
handleTaskFileError(error, 'set-status', options);
}
});
program
.command('read-task')
.description('读取单个任务的详细信息')
.option('--tasks-path <path>', '任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.requiredOption('--task-id <id>', '要读取的任务ID(支持复合ID,如"2.1")')
.action(async (options) => {
console.log(chalk.cyan('📖 开始读取任务详情...'));
try {
const projectDir = process.cwd();
console.log(chalk.blue('📋 任务文件: ') + chalk.white(options.tasksPath));
console.log(chalk.blue('🎯 任务ID: ') + chalk.white(options.taskId));
const result = await readTask(projectDir, options.tasksPath, String(options.taskId) // 确保 taskId 是字符串
);
if (!result.task) {
console.log(chalk.yellow('\n⚠️ ' + result.message));
process.exit(1);
}
// 打印任务详情
console.log(chalk.blue('\n📝 任务详情:'));
console.log(chalk.cyan('ID: ') + chalk.white(result.task.id));
console.log(chalk.cyan('标题: ') + chalk.white(result.task.title));
console.log(chalk.cyan('描述: ') + chalk.white(result.task.description));
const statusColor = result.task.status === 'done' ? chalk.green :
result.task.status === 'in-progress' ? chalk.yellow :
chalk.gray;
console.log(chalk.cyan('状态: ') + statusColor(result.task.status));
const priorityColor = result.task.priority === 'high' ? chalk.red :
result.task.priority === 'medium' ? chalk.yellow :
chalk.blue;
console.log(chalk.cyan('优先级: ') + priorityColor(result.task.priority));
if (result.task.dependencies && result.task.dependencies.length > 0) {
console.log(chalk.cyan('依赖任务: ') + chalk.white(result.task.dependencies.join(', ')));
}
else {
console.log(chalk.cyan('依赖任务: ') + chalk.gray('无'));
}
// 如果任务已完成且有完成总结,显示完成总结
if (result.task.status === 'done' && result.task.completionSummary) {
console.log(chalk.blue('\n📑 完成总结:'));
console.log(chalk.white(result.task.completionSummary));
}
console.log(chalk.blue('\n📄 详细信息:'));
console.log(chalk.white(result.task.details));
console.log(chalk.blue('\n🧪 测试策略:'));
console.log(chalk.white(result.task.testStrategy));
// 如果任务有子任务,显示子任务数量
if (result.task.subTasks && result.task.subTasks.length > 0) {
console.log(chalk.blue('\n👥 子任务:'));
console.log(chalk.white(`共有 ${result.task.subTasks.length} 个子任务`));
console.log(chalk.gray('使用 list-tasks 命令查看所有子任务'));
}
}
catch (error) {
handleTaskFileError(error, 'read-task', options);
}
});
program
.command('breakup-task')
.description('将任务分解为子任务')
.option('--tasks-path <path>', '任务文件路径', process.env.TASKS_PATH || 'tasks/tasks.json')
.requiredOption('--task-id <id>', '要分解的任务ID')
.option('--prompt <text>', '用于分解任务的提示内容')
.action(async (options) => {
console.log(chalk.cyan('🔄 开始分解任务...'));
// 从环境变量获取配置
const openaiUrl = process.env.OPENAI_API_URL;
const apiKey = process.env.OPENAI_API_KEY;
const model = process.env.OPENAI_MODEL;
const streamMode = process.env.STREAM_MODE === 'true';
// 验证必要的环境变量
if (!openaiUrl) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 OpenAI 兼容 URL。请在.env.octm文件中设置 OPENAI_API_URL。'));
process.exit(1);
}
if (!apiKey) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供 API 密钥。请在.env.octm文件中设置 OPENAI_API_KEY。'));
process.exit(1);
}
if (!model) {
console.error(chalk.bold.red('错误: ') + chalk.red('未提供模型名称。请在.env.octm文件中设置 OPENAI_MODEL。'));
process.exit(1);
}
try {
console.log(chalk.blue('📋 任务文件: ') + chalk.white(options.tasksPath));
console.log(chalk.blue('🎯 任务ID: ') + chalk.white(options.taskId));
if (options.prompt) {
console.log(chalk.blue('💡 分解提示: ') + chalk.white(options.prompt));
}
// 调用breakupTask函数
console.log(chalk.blue('🧠 正在分解任务...'));
const subTasks = await breakupTask({
taskId: String(options.taskId), // 确保 taskId 是字符串
prompt: options.prompt,
tasksPath: options.tasksPath,
apiKey,
apiUrl: openaiUrl,
model,
streamMode
});
console.log(chalk.bold.green('\n✅ 任务分解完成!'));
console.log(chalk.white(`共生成 ${subTasks.length} 个子任务,已保存到: ${options.tasksPath}`));
// 打印子任务列表预览
console.log(chalk.blue('\n📋 子任务列表预览:'));
subTasks.forEach(task => {
console.log(chalk.green(`[${task.id}] `) + chalk.white(task.title) + chalk.gray(` (优先级: ${task.priority})`));
});
}
catch (error) {
handleTaskFileError(error, 'breakup-task', options);
}
});
// 解析命令行参数
program.parse(process.argv);
//# sourceMappingURL=command.js.map