UNPKG

openai-compatible-task-master

Version:

使用MCP解析PRD文档并生成任务列表

810 lines 40.5 kB
#!/usr/bin/env node 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