This is page 50 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /scripts/modules/commands.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * commands.js 3 | * Command-line interface for the Task Master CLI 4 | */ 5 | 6 | import { Command } from 'commander'; 7 | import path from 'path'; 8 | import chalk from 'chalk'; 9 | import boxen from 'boxen'; 10 | import fs from 'fs'; 11 | import https from 'https'; 12 | import http from 'http'; 13 | import inquirer from 'inquirer'; 14 | import search from '@inquirer/search'; 15 | import ora from 'ora'; // Import ora 16 | 17 | import { log, readJSON } from './utils.js'; 18 | // Import new commands from @tm/cli 19 | import { 20 | ListTasksCommand, 21 | ShowCommand, 22 | AuthCommand, 23 | ContextCommand, 24 | StartCommand, 25 | SetStatusCommand, 26 | checkForUpdate, 27 | performAutoUpdate, 28 | displayUpgradeNotification 29 | } from '@tm/cli'; 30 | 31 | import { 32 | parsePRD, 33 | updateTasks, 34 | generateTaskFiles, 35 | listTasks, 36 | expandTask, 37 | expandAllTasks, 38 | clearSubtasks, 39 | addTask, 40 | addSubtask, 41 | removeSubtask, 42 | analyzeTaskComplexity, 43 | updateTaskById, 44 | updateSubtaskById, 45 | removeTask, 46 | findTaskById, 47 | taskExists, 48 | moveTask, 49 | migrateProject, 50 | setResponseLanguage, 51 | scopeUpTask, 52 | scopeDownTask, 53 | validateStrength 54 | } from './task-manager.js'; 55 | 56 | import { 57 | moveTasksBetweenTags, 58 | MoveTaskError, 59 | MOVE_ERROR_CODES 60 | } from './task-manager/move-task.js'; 61 | 62 | import { 63 | createTag, 64 | deleteTag, 65 | tags, 66 | useTag, 67 | renameTag, 68 | copyTag 69 | } from './task-manager/tag-management.js'; 70 | 71 | import { 72 | addDependency, 73 | removeDependency, 74 | validateDependenciesCommand, 75 | fixDependenciesCommand, 76 | DependencyError, 77 | DEPENDENCY_ERROR_CODES 78 | } from './dependency-manager.js'; 79 | 80 | import { 81 | isApiKeySet, 82 | getDebugFlag, 83 | getConfig, 84 | writeConfig, 85 | ConfigurationError, 86 | isConfigFilePresent, 87 | getAvailableModels, 88 | getBaseUrlForRole, 89 | getDefaultNumTasks 90 | } from './config-manager.js'; 91 | 92 | import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js'; 93 | 94 | import { 95 | COMPLEXITY_REPORT_FILE, 96 | TASKMASTER_TASKS_FILE, 97 | TASKMASTER_DOCS_DIR 98 | } from '../../src/constants/paths.js'; 99 | 100 | import { initTaskMaster } from '../../src/task-master.js'; 101 | 102 | import { 103 | displayBanner, 104 | displayHelp, 105 | displayNextTask, 106 | displayTaskById, 107 | displayComplexityReport, 108 | getStatusWithColor, 109 | confirmTaskOverwrite, 110 | startLoadingIndicator, 111 | stopLoadingIndicator, 112 | displayModelConfiguration, 113 | displayAvailableModels, 114 | displayApiKeyStatus, 115 | displayAiUsageSummary, 116 | displayMultipleTasksSummary, 117 | displayTaggedTasksFYI, 118 | displayCurrentTagIndicator, 119 | displayCrossTagDependencyError, 120 | displaySubtaskMoveError, 121 | displayInvalidTagCombinationError, 122 | displayDependencyValidationHints 123 | } from './ui.js'; 124 | import { 125 | confirmProfilesRemove, 126 | confirmRemoveAllRemainingProfiles 127 | } from '../../src/ui/confirm.js'; 128 | import { 129 | wouldRemovalLeaveNoProfiles, 130 | getInstalledProfiles 131 | } from '../../src/utils/profiles.js'; 132 | 133 | import { initializeProject } from '../init.js'; 134 | import { 135 | getModelConfiguration, 136 | getAvailableModelsList, 137 | setModel, 138 | getApiKeyStatusReport 139 | } from './task-manager/models.js'; 140 | import { 141 | isValidTaskStatus, 142 | TASK_STATUS_OPTIONS 143 | } from '../../src/constants/task-status.js'; 144 | import { 145 | isValidRulesAction, 146 | RULES_ACTIONS, 147 | RULES_SETUP_ACTION 148 | } from '../../src/constants/rules-actions.js'; 149 | import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; 150 | import { syncTasksToReadme } from './sync-readme.js'; 151 | import { RULE_PROFILES } from '../../src/constants/profiles.js'; 152 | import { 153 | convertAllRulesToProfileRules, 154 | removeProfileRules, 155 | isValidProfile, 156 | getRulesProfile 157 | } from '../../src/utils/rule-transformer.js'; 158 | import { 159 | runInteractiveProfilesSetup, 160 | generateProfileSummary, 161 | categorizeProfileResults, 162 | generateProfileRemovalSummary, 163 | categorizeRemovalResults 164 | } from '../../src/utils/profiles.js'; 165 | 166 | /** 167 | * Runs the interactive setup process for model configuration. 168 | * @param {string|null} projectRoot - The resolved project root directory. 169 | */ 170 | async function runInteractiveSetup(projectRoot) { 171 | if (!projectRoot) { 172 | console.error( 173 | chalk.red( 174 | 'Error: Could not determine project root for interactive setup.' 175 | ) 176 | ); 177 | process.exit(1); 178 | } 179 | 180 | const currentConfigResult = await getModelConfiguration({ projectRoot }); 181 | const currentModels = currentConfigResult.success 182 | ? currentConfigResult.data.activeModels 183 | : { main: null, research: null, fallback: null }; 184 | // Handle potential config load failure gracefully for the setup flow 185 | if ( 186 | !currentConfigResult.success && 187 | currentConfigResult.error?.code !== 'CONFIG_MISSING' 188 | ) { 189 | console.warn( 190 | chalk.yellow( 191 | `Warning: Could not load current model configuration: ${currentConfigResult.error?.message || 'Unknown error'}. Proceeding with defaults.` 192 | ) 193 | ); 194 | } 195 | 196 | // Helper function to fetch OpenRouter models (duplicated for CLI context) 197 | function fetchOpenRouterModelsCLI() { 198 | return new Promise((resolve) => { 199 | const options = { 200 | hostname: 'openrouter.ai', 201 | path: '/api/v1/models', 202 | method: 'GET', 203 | headers: { 204 | Accept: 'application/json' 205 | } 206 | }; 207 | 208 | const req = https.request(options, (res) => { 209 | let data = ''; 210 | res.on('data', (chunk) => { 211 | data += chunk; 212 | }); 213 | res.on('end', () => { 214 | if (res.statusCode === 200) { 215 | try { 216 | const parsedData = JSON.parse(data); 217 | resolve(parsedData.data || []); // Return the array of models 218 | } catch (e) { 219 | console.error('Error parsing OpenRouter response:', e); 220 | resolve(null); // Indicate failure 221 | } 222 | } else { 223 | console.error( 224 | `OpenRouter API request failed with status code: ${res.statusCode}` 225 | ); 226 | resolve(null); // Indicate failure 227 | } 228 | }); 229 | }); 230 | 231 | req.on('error', (e) => { 232 | console.error('Error fetching OpenRouter models:', e); 233 | resolve(null); // Indicate failure 234 | }); 235 | req.end(); 236 | }); 237 | } 238 | 239 | // Helper function to fetch Ollama models (duplicated for CLI context) 240 | function fetchOllamaModelsCLI(baseURL = 'http://localhost:11434/api') { 241 | return new Promise((resolve) => { 242 | try { 243 | // Parse the base URL to extract hostname, port, and base path 244 | const url = new URL(baseURL); 245 | const isHttps = url.protocol === 'https:'; 246 | const port = url.port || (isHttps ? 443 : 80); 247 | const basePath = url.pathname.endsWith('/') 248 | ? url.pathname.slice(0, -1) 249 | : url.pathname; 250 | 251 | const options = { 252 | hostname: url.hostname, 253 | port: parseInt(port, 10), 254 | path: `${basePath}/tags`, 255 | method: 'GET', 256 | headers: { 257 | Accept: 'application/json' 258 | } 259 | }; 260 | 261 | const requestLib = isHttps ? https : http; 262 | const req = requestLib.request(options, (res) => { 263 | let data = ''; 264 | res.on('data', (chunk) => { 265 | data += chunk; 266 | }); 267 | res.on('end', () => { 268 | if (res.statusCode === 200) { 269 | try { 270 | const parsedData = JSON.parse(data); 271 | resolve(parsedData.models || []); // Return the array of models 272 | } catch (e) { 273 | console.error('Error parsing Ollama response:', e); 274 | resolve(null); // Indicate failure 275 | } 276 | } else { 277 | console.error( 278 | `Ollama API request failed with status code: ${res.statusCode}` 279 | ); 280 | resolve(null); // Indicate failure 281 | } 282 | }); 283 | }); 284 | 285 | req.on('error', (e) => { 286 | console.error('Error fetching Ollama models:', e); 287 | resolve(null); // Indicate failure 288 | }); 289 | req.end(); 290 | } catch (e) { 291 | console.error('Error parsing Ollama base URL:', e); 292 | resolve(null); // Indicate failure 293 | } 294 | }); 295 | } 296 | 297 | // Helper to get choices and default index for a role 298 | const getPromptData = (role, allowNone = false) => { 299 | const currentModel = currentModels[role]; // Use the fetched data 300 | const allModelsRaw = getAvailableModels(); // Get all available models 301 | 302 | // Manually group models by provider 303 | const modelsByProvider = allModelsRaw.reduce((acc, model) => { 304 | if (!acc[model.provider]) { 305 | acc[model.provider] = []; 306 | } 307 | acc[model.provider].push(model); 308 | return acc; 309 | }, {}); 310 | 311 | const cancelOption = { name: '⏹ Cancel Model Setup', value: '__CANCEL__' }; // Symbol updated 312 | const noChangeOption = currentModel?.modelId 313 | ? { 314 | name: `✔ No change to current ${role} model (${currentModel.modelId})`, // Symbol updated 315 | value: '__NO_CHANGE__' 316 | } 317 | : null; 318 | 319 | // Define custom provider options 320 | const customProviderOptions = [ 321 | { name: '* Custom OpenRouter model', value: '__CUSTOM_OPENROUTER__' }, 322 | { name: '* Custom Ollama model', value: '__CUSTOM_OLLAMA__' }, 323 | { name: '* Custom Bedrock model', value: '__CUSTOM_BEDROCK__' }, 324 | { name: '* Custom Azure model', value: '__CUSTOM_AZURE__' }, 325 | { name: '* Custom Vertex model', value: '__CUSTOM_VERTEX__' } 326 | ]; 327 | 328 | let choices = []; 329 | let defaultIndex = 0; // Default to 'Cancel' 330 | 331 | // Filter and format models allowed for this role using the manually grouped data 332 | const roleChoices = Object.entries(modelsByProvider) 333 | .map(([provider, models]) => { 334 | const providerModels = models 335 | .filter((m) => m.allowed_roles.includes(role)) 336 | .map((m) => ({ 337 | name: `${provider} / ${m.id} ${ 338 | m.cost_per_1m_tokens 339 | ? chalk.gray( 340 | `($${m.cost_per_1m_tokens.input.toFixed(2)} input | $${m.cost_per_1m_tokens.output.toFixed(2)} output)` 341 | ) 342 | : '' 343 | }`, 344 | value: { id: m.id, provider }, 345 | short: `${provider}/${m.id}` 346 | })); 347 | if (providerModels.length > 0) { 348 | return [...providerModels]; 349 | } 350 | return null; 351 | }) 352 | .filter(Boolean) 353 | .flat(); 354 | 355 | // Find the index of the currently selected model for setting the default 356 | let currentChoiceIndex = -1; 357 | if (currentModel?.modelId && currentModel?.provider) { 358 | currentChoiceIndex = roleChoices.findIndex( 359 | (choice) => 360 | typeof choice.value === 'object' && 361 | choice.value.id === currentModel.modelId && 362 | choice.value.provider === currentModel.provider 363 | ); 364 | } 365 | 366 | // Construct final choices list with custom options moved to bottom 367 | const systemOptions = []; 368 | if (noChangeOption) { 369 | systemOptions.push(noChangeOption); 370 | } 371 | systemOptions.push(cancelOption); 372 | 373 | const systemLength = systemOptions.length; 374 | 375 | if (allowNone) { 376 | choices = [ 377 | ...systemOptions, 378 | new inquirer.Separator('\n── Standard Models ──'), 379 | { name: '⚪ None (disable)', value: null }, 380 | ...roleChoices, 381 | new inquirer.Separator('\n── Custom Providers ──'), 382 | ...customProviderOptions 383 | ]; 384 | // Adjust default index: System + Sep1 + None (+2) 385 | const noneOptionIndex = systemLength + 1; 386 | defaultIndex = 387 | currentChoiceIndex !== -1 388 | ? currentChoiceIndex + systemLength + 2 // Offset by system options and separators 389 | : noneOptionIndex; // Default to 'None' if no current model matched 390 | } else { 391 | choices = [ 392 | ...systemOptions, 393 | new inquirer.Separator('\n── Standard Models ──'), 394 | ...roleChoices, 395 | new inquirer.Separator('\n── Custom Providers ──'), 396 | ...customProviderOptions 397 | ]; 398 | // Adjust default index: System + Sep (+1) 399 | defaultIndex = 400 | currentChoiceIndex !== -1 401 | ? currentChoiceIndex + systemLength + 1 // Offset by system options and separator 402 | : noChangeOption 403 | ? 1 404 | : 0; // Default to 'No Change' if present, else 'Cancel' 405 | } 406 | 407 | // Ensure defaultIndex is valid within the final choices array length 408 | if (defaultIndex < 0 || defaultIndex >= choices.length) { 409 | // If default calculation failed or pointed outside bounds, reset intelligently 410 | defaultIndex = 0; // Default to 'Cancel' 411 | console.warn( 412 | `Warning: Could not determine default model for role '${role}'. Defaulting to 'Cancel'.` 413 | ); // Add warning 414 | } 415 | 416 | return { choices, default: defaultIndex }; 417 | }; 418 | 419 | // --- Generate choices using the helper --- 420 | const mainPromptData = getPromptData('main'); 421 | const researchPromptData = getPromptData('research'); 422 | const fallbackPromptData = getPromptData('fallback', true); // Allow 'None' for fallback 423 | 424 | // Display helpful intro message 425 | console.log(chalk.cyan('\n🎯 Interactive Model Setup')); 426 | console.log(chalk.gray('━'.repeat(50))); 427 | console.log(chalk.yellow('💡 Navigation tips:')); 428 | console.log(chalk.gray(' • Type to search and filter options')); 429 | console.log(chalk.gray(' • Use ↑↓ arrow keys to navigate results')); 430 | console.log( 431 | chalk.gray( 432 | ' • Standard models are listed first, custom providers at bottom' 433 | ) 434 | ); 435 | console.log(chalk.gray(' • Press Enter to select\n')); 436 | 437 | // Helper function to create search source for models 438 | const createSearchSource = (choices, defaultValue) => { 439 | return (searchTerm = '') => { 440 | const filteredChoices = choices.filter((choice) => { 441 | if (choice.type === 'separator') return true; // Always show separators 442 | const searchText = choice.name || ''; 443 | return searchText.toLowerCase().includes(searchTerm.toLowerCase()); 444 | }); 445 | return Promise.resolve(filteredChoices); 446 | }; 447 | }; 448 | 449 | const answers = {}; 450 | 451 | // Main model selection 452 | answers.mainModel = await search({ 453 | message: 'Select the main model for generation/updates:', 454 | source: createSearchSource(mainPromptData.choices, mainPromptData.default), 455 | pageSize: 15 456 | }); 457 | 458 | if (answers.mainModel !== '__CANCEL__') { 459 | // Research model selection 460 | answers.researchModel = await search({ 461 | message: 'Select the research model:', 462 | source: createSearchSource( 463 | researchPromptData.choices, 464 | researchPromptData.default 465 | ), 466 | pageSize: 15 467 | }); 468 | 469 | if (answers.researchModel !== '__CANCEL__') { 470 | // Fallback model selection 471 | answers.fallbackModel = await search({ 472 | message: 'Select the fallback model (optional):', 473 | source: createSearchSource( 474 | fallbackPromptData.choices, 475 | fallbackPromptData.default 476 | ), 477 | pageSize: 15 478 | }); 479 | } 480 | } 481 | 482 | let setupSuccess = true; 483 | let setupConfigModified = false; 484 | const coreOptionsSetup = { projectRoot }; // Pass root for setup actions 485 | 486 | // Helper to handle setting a model (including custom) 487 | async function handleSetModel(role, selectedValue, currentModelId) { 488 | if (selectedValue === '__CANCEL__') { 489 | console.log( 490 | chalk.yellow(`\nSetup canceled during ${role} model selection.`) 491 | ); 492 | setupSuccess = false; // Also mark success as false on cancel 493 | return false; // Indicate cancellation 494 | } 495 | 496 | // Handle the new 'No Change' option 497 | if (selectedValue === '__NO_CHANGE__') { 498 | console.log(chalk.gray(`No change selected for ${role} model.`)); 499 | return true; // Indicate success, continue setup 500 | } 501 | 502 | let modelIdToSet = null; 503 | let providerHint = null; 504 | let isCustomSelection = false; 505 | 506 | if (selectedValue === '__CUSTOM_OPENROUTER__') { 507 | isCustomSelection = true; 508 | const { customId } = await inquirer.prompt([ 509 | { 510 | type: 'input', 511 | name: 'customId', 512 | message: `Enter the custom OpenRouter Model ID for the ${role} role:` 513 | } 514 | ]); 515 | if (!customId) { 516 | console.log(chalk.yellow('No custom ID entered. Skipping role.')); 517 | return true; // Continue setup, but don't set this role 518 | } 519 | modelIdToSet = customId; 520 | providerHint = CUSTOM_PROVIDERS.OPENROUTER; 521 | // Validate against live OpenRouter list 522 | const openRouterModels = await fetchOpenRouterModelsCLI(); 523 | if ( 524 | !openRouterModels || 525 | !openRouterModels.some((m) => m.id === modelIdToSet) 526 | ) { 527 | console.error( 528 | chalk.red( 529 | `Error: Model ID "${modelIdToSet}" not found in the live OpenRouter model list. Please check the ID.` 530 | ) 531 | ); 532 | setupSuccess = false; 533 | return true; // Continue setup, but mark as failed 534 | } 535 | } else if (selectedValue === '__CUSTOM_OLLAMA__') { 536 | isCustomSelection = true; 537 | const { customId } = await inquirer.prompt([ 538 | { 539 | type: 'input', 540 | name: 'customId', 541 | message: `Enter the custom Ollama Model ID for the ${role} role:` 542 | } 543 | ]); 544 | if (!customId) { 545 | console.log(chalk.yellow('No custom ID entered. Skipping role.')); 546 | return true; // Continue setup, but don't set this role 547 | } 548 | modelIdToSet = customId; 549 | providerHint = CUSTOM_PROVIDERS.OLLAMA; 550 | // Get the Ollama base URL from config for this role 551 | const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); 552 | // Validate against live Ollama list 553 | const ollamaModels = await fetchOllamaModelsCLI(ollamaBaseURL); 554 | if (ollamaModels === null) { 555 | console.error( 556 | chalk.red( 557 | `Error: Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` 558 | ) 559 | ); 560 | setupSuccess = false; 561 | return true; // Continue setup, but mark as failed 562 | } else if (!ollamaModels.some((m) => m.model === modelIdToSet)) { 563 | console.error( 564 | chalk.red( 565 | `Error: Model ID "${modelIdToSet}" not found in the Ollama instance. Please verify the model is pulled and available.` 566 | ) 567 | ); 568 | console.log( 569 | chalk.yellow( 570 | `You can check available models with: curl ${ollamaBaseURL}/tags` 571 | ) 572 | ); 573 | setupSuccess = false; 574 | return true; // Continue setup, but mark as failed 575 | } 576 | } else if (selectedValue === '__CUSTOM_BEDROCK__') { 577 | isCustomSelection = true; 578 | const { customId } = await inquirer.prompt([ 579 | { 580 | type: 'input', 581 | name: 'customId', 582 | message: `Enter the custom Bedrock Model ID for the ${role} role (e.g., anthropic.claude-3-sonnet-20240229-v1:0):` 583 | } 584 | ]); 585 | if (!customId) { 586 | console.log(chalk.yellow('No custom ID entered. Skipping role.')); 587 | return true; // Continue setup, but don't set this role 588 | } 589 | modelIdToSet = customId; 590 | providerHint = CUSTOM_PROVIDERS.BEDROCK; 591 | 592 | // Check if AWS environment variables exist 593 | if ( 594 | !process.env.AWS_ACCESS_KEY_ID || 595 | !process.env.AWS_SECRET_ACCESS_KEY 596 | ) { 597 | console.warn( 598 | chalk.yellow( 599 | 'Warning: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Will fallback to system configuration. (ex: aws config files or ec2 instance profiles)' 600 | ) 601 | ); 602 | setupSuccess = false; 603 | return true; // Continue setup, but mark as failed 604 | } 605 | 606 | console.log( 607 | chalk.blue( 608 | `Custom Bedrock model "${modelIdToSet}" will be used. No validation performed.` 609 | ) 610 | ); 611 | } else if (selectedValue === '__CUSTOM_AZURE__') { 612 | isCustomSelection = true; 613 | const { customId } = await inquirer.prompt([ 614 | { 615 | type: 'input', 616 | name: 'customId', 617 | message: `Enter the custom Azure OpenAI Model ID for the ${role} role (e.g., gpt-4o):` 618 | } 619 | ]); 620 | if (!customId) { 621 | console.log(chalk.yellow('No custom ID entered. Skipping role.')); 622 | return true; // Continue setup, but don't set this role 623 | } 624 | modelIdToSet = customId; 625 | providerHint = CUSTOM_PROVIDERS.AZURE; 626 | 627 | // Check if Azure environment variables exist 628 | if ( 629 | !process.env.AZURE_OPENAI_API_KEY || 630 | !process.env.AZURE_OPENAI_ENDPOINT 631 | ) { 632 | console.error( 633 | chalk.red( 634 | 'Error: AZURE_OPENAI_API_KEY and/or AZURE_OPENAI_ENDPOINT environment variables are missing. Please set them before using custom Azure models.' 635 | ) 636 | ); 637 | setupSuccess = false; 638 | return true; // Continue setup, but mark as failed 639 | } 640 | 641 | console.log( 642 | chalk.blue( 643 | `Custom Azure OpenAI model "${modelIdToSet}" will be used. No validation performed.` 644 | ) 645 | ); 646 | } else if (selectedValue === '__CUSTOM_VERTEX__') { 647 | isCustomSelection = true; 648 | const { customId } = await inquirer.prompt([ 649 | { 650 | type: 'input', 651 | name: 'customId', 652 | message: `Enter the custom Vertex AI Model ID for the ${role} role (e.g., gemini-1.5-pro-002):` 653 | } 654 | ]); 655 | if (!customId) { 656 | console.log(chalk.yellow('No custom ID entered. Skipping role.')); 657 | return true; // Continue setup, but don't set this role 658 | } 659 | modelIdToSet = customId; 660 | providerHint = CUSTOM_PROVIDERS.VERTEX; 661 | 662 | // Check if Google/Vertex environment variables exist 663 | if ( 664 | !process.env.GOOGLE_API_KEY && 665 | !process.env.GOOGLE_APPLICATION_CREDENTIALS 666 | ) { 667 | console.error( 668 | chalk.red( 669 | 'Error: Either GOOGLE_API_KEY or GOOGLE_APPLICATION_CREDENTIALS environment variable is required. Please set one before using custom Vertex models.' 670 | ) 671 | ); 672 | setupSuccess = false; 673 | return true; // Continue setup, but mark as failed 674 | } 675 | 676 | console.log( 677 | chalk.blue( 678 | `Custom Vertex AI model "${modelIdToSet}" will be used. No validation performed.` 679 | ) 680 | ); 681 | } else if ( 682 | selectedValue && 683 | typeof selectedValue === 'object' && 684 | selectedValue.id 685 | ) { 686 | // Standard model selected from list 687 | modelIdToSet = selectedValue.id; 688 | providerHint = selectedValue.provider; // Provider is known 689 | } else if (selectedValue === null && role === 'fallback') { 690 | // Handle disabling fallback 691 | modelIdToSet = null; 692 | providerHint = null; 693 | } else if (selectedValue) { 694 | console.error( 695 | chalk.red( 696 | `Internal Error: Unexpected selection value for ${role}: ${JSON.stringify(selectedValue)}` 697 | ) 698 | ); 699 | setupSuccess = false; 700 | return true; 701 | } 702 | 703 | // Only proceed if there's a change to be made 704 | if (modelIdToSet !== currentModelId) { 705 | if (modelIdToSet) { 706 | // Set a specific model (standard or custom) 707 | const result = await setModel(role, modelIdToSet, { 708 | ...coreOptionsSetup, 709 | providerHint // Pass the hint 710 | }); 711 | if (result.success) { 712 | console.log( 713 | chalk.blue( 714 | `Set ${role} model: ${result.data.provider} / ${result.data.modelId}` 715 | ) 716 | ); 717 | if (result.data.warning) { 718 | // Display warning if returned by setModel 719 | console.log(chalk.yellow(result.data.warning)); 720 | } 721 | setupConfigModified = true; 722 | } else { 723 | console.error( 724 | chalk.red( 725 | `Error setting ${role} model: ${result.error?.message || 'Unknown'}` 726 | ) 727 | ); 728 | setupSuccess = false; 729 | } 730 | } else if (role === 'fallback') { 731 | // Disable fallback model 732 | const currentCfg = getConfig(projectRoot); 733 | if (currentCfg?.models?.fallback?.modelId) { 734 | // Check if it was actually set before clearing 735 | currentCfg.models.fallback = { 736 | ...currentCfg.models.fallback, 737 | provider: undefined, 738 | modelId: undefined 739 | }; 740 | if (writeConfig(currentCfg, projectRoot)) { 741 | console.log(chalk.blue('Fallback model disabled.')); 742 | setupConfigModified = true; 743 | } else { 744 | console.error( 745 | chalk.red('Failed to disable fallback model in config file.') 746 | ); 747 | setupSuccess = false; 748 | } 749 | } else { 750 | console.log(chalk.blue('Fallback model was already disabled.')); 751 | } 752 | } 753 | } 754 | return true; // Indicate setup should continue 755 | } 756 | 757 | // Process answers using the handler 758 | if ( 759 | !(await handleSetModel( 760 | 'main', 761 | answers.mainModel, 762 | currentModels.main?.modelId // <--- Now 'currentModels' is defined 763 | )) 764 | ) { 765 | return false; // Explicitly return false if cancelled 766 | } 767 | if ( 768 | !(await handleSetModel( 769 | 'research', 770 | answers.researchModel, 771 | currentModels.research?.modelId // <--- Now 'currentModels' is defined 772 | )) 773 | ) { 774 | return false; // Explicitly return false if cancelled 775 | } 776 | if ( 777 | !(await handleSetModel( 778 | 'fallback', 779 | answers.fallbackModel, 780 | currentModels.fallback?.modelId // <--- Now 'currentModels' is defined 781 | )) 782 | ) { 783 | return false; // Explicitly return false if cancelled 784 | } 785 | 786 | if (setupSuccess && setupConfigModified) { 787 | console.log(chalk.green.bold('\nModel setup complete!')); 788 | } else if (setupSuccess && !setupConfigModified) { 789 | console.log(chalk.yellow('\nNo changes made to model configuration.')); 790 | } else if (!setupSuccess) { 791 | console.error( 792 | chalk.red( 793 | '\nErrors occurred during model selection. Please review and try again.' 794 | ) 795 | ); 796 | } 797 | return true; // Indicate setup flow completed (not cancelled) 798 | // Let the main command flow continue to display results 799 | } 800 | 801 | /** 802 | * Configure and register CLI commands 803 | * @param {Object} program - Commander program instance 804 | */ 805 | function registerCommands(programInstance) { 806 | // Add global error handler for unknown options 807 | programInstance.on('option:unknown', function (unknownOption) { 808 | const commandName = this._name || 'unknown'; 809 | console.error(chalk.red(`Error: Unknown option '${unknownOption}'`)); 810 | console.error( 811 | chalk.yellow( 812 | `Run 'task-master ${commandName} --help' to see available options` 813 | ) 814 | ); 815 | process.exit(1); 816 | }); 817 | 818 | // parse-prd command 819 | programInstance 820 | .command('parse-prd') 821 | .description('Parse a PRD file and generate tasks') 822 | .argument('[file]', 'Path to the PRD file') 823 | .option( 824 | '-i, --input <file>', 825 | 'Path to the PRD file (alternative to positional argument)' 826 | ) 827 | .option('-o, --output <file>', 'Output file path') 828 | .option( 829 | '-n, --num-tasks <number>', 830 | 'Number of tasks to generate', 831 | getDefaultNumTasks() 832 | ) 833 | .option('-f, --force', 'Skip confirmation when overwriting existing tasks') 834 | .option( 835 | '--append', 836 | 'Append new tasks to existing tasks.json instead of overwriting' 837 | ) 838 | .option( 839 | '-r, --research', 840 | 'Use Perplexity AI for research-backed task generation, providing more comprehensive and accurate task breakdown' 841 | ) 842 | .option('--tag <tag>', 'Specify tag context for task operations') 843 | .action(async (file, options) => { 844 | // Initialize TaskMaster 845 | let taskMaster; 846 | try { 847 | const initOptions = { 848 | prdPath: file || options.input || true, 849 | tag: options.tag 850 | }; 851 | // Only include tasksPath if output is explicitly specified 852 | if (options.output) { 853 | initOptions.tasksPath = options.output; 854 | } 855 | taskMaster = initTaskMaster(initOptions); 856 | } catch (error) { 857 | console.log( 858 | boxen( 859 | `${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd <prd-file.txt> [options]\n\n${chalk.cyan('Options:')}\n -i, --input <file> Path to the PRD file (alternative to positional argument)\n -o, --output <file> Output file path (default: .taskmaster/tasks/tasks.json)\n -n, --num-tasks <number> Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, 860 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 861 | ) 862 | ); 863 | console.error(chalk.red(`\nError: ${error.message}`)); 864 | process.exit(1); 865 | } 866 | 867 | const numTasks = parseInt(options.numTasks, 10); 868 | const force = options.force || false; 869 | const append = options.append || false; 870 | const research = options.research || false; 871 | let useForce = force; 872 | const useAppend = append; 873 | 874 | // Resolve tag using standard pattern 875 | const tag = taskMaster.getCurrentTag(); 876 | 877 | // Show current tag context 878 | displayCurrentTagIndicator(tag); 879 | 880 | // Helper function to check if there are existing tasks in the target tag and confirm overwrite 881 | async function confirmOverwriteIfNeeded() { 882 | // Check if there are existing tasks in the target tag 883 | let hasExistingTasksInTag = false; 884 | const tasksPath = taskMaster.getTasksPath(); 885 | if (fs.existsSync(tasksPath)) { 886 | try { 887 | // Read the entire file to check if the tag exists 888 | const existingFileContent = fs.readFileSync(tasksPath, 'utf8'); 889 | const allData = JSON.parse(existingFileContent); 890 | 891 | // Check if the target tag exists and has tasks 892 | if ( 893 | allData[tag] && 894 | Array.isArray(allData[tag].tasks) && 895 | allData[tag].tasks.length > 0 896 | ) { 897 | hasExistingTasksInTag = true; 898 | } 899 | } catch (error) { 900 | // If we can't read the file or parse it, assume no existing tasks in this tag 901 | hasExistingTasksInTag = false; 902 | } 903 | } 904 | 905 | // Only show confirmation if there are existing tasks in the target tag 906 | if (hasExistingTasksInTag && !useForce && !useAppend) { 907 | const overwrite = await confirmTaskOverwrite(tasksPath); 908 | if (!overwrite) { 909 | log('info', 'Operation cancelled.'); 910 | return false; 911 | } 912 | // If user confirms 'y', we should set useForce = true for the parsePRD call 913 | // Only overwrite if not appending 914 | useForce = true; 915 | } 916 | return true; 917 | } 918 | 919 | try { 920 | if (!(await confirmOverwriteIfNeeded())) return; 921 | 922 | console.log(chalk.blue(`Parsing PRD file: ${taskMaster.getPrdPath()}`)); 923 | console.log(chalk.blue(`Generating ${numTasks} tasks...`)); 924 | if (append) { 925 | console.log(chalk.blue('Appending to existing tasks...')); 926 | } 927 | if (research) { 928 | console.log( 929 | chalk.blue( 930 | 'Using Perplexity AI for research-backed task generation' 931 | ) 932 | ); 933 | } 934 | 935 | // Handle case where getTasksPath() returns null 936 | const outputPath = 937 | taskMaster.getTasksPath() || 938 | path.join(taskMaster.getProjectRoot(), TASKMASTER_TASKS_FILE); 939 | await parsePRD(taskMaster.getPrdPath(), outputPath, numTasks, { 940 | append: useAppend, 941 | force: useForce, 942 | research: research, 943 | projectRoot: taskMaster.getProjectRoot(), 944 | tag: tag 945 | }); 946 | } catch (error) { 947 | console.error(chalk.red(`Error parsing PRD: ${error.message}`)); 948 | process.exit(1); 949 | } 950 | }); 951 | 952 | // update command 953 | programInstance 954 | .command('update') 955 | .description( 956 | 'Update multiple tasks with ID >= "from" based on new information or implementation changes' 957 | ) 958 | .option( 959 | '-f, --file <file>', 960 | 'Path to the tasks file', 961 | TASKMASTER_TASKS_FILE 962 | ) 963 | .option( 964 | '--from <id>', 965 | 'Task ID to start updating from (tasks with ID >= this value will be updated)', 966 | '1' 967 | ) 968 | .option( 969 | '-p, --prompt <text>', 970 | 'Prompt explaining the changes or new context (required)' 971 | ) 972 | .option( 973 | '-r, --research', 974 | 'Use Perplexity AI for research-backed task updates' 975 | ) 976 | .option('--tag <tag>', 'Specify tag context for task operations') 977 | .action(async (options) => { 978 | // Initialize TaskMaster 979 | const taskMaster = initTaskMaster({ 980 | tasksPath: options.file || true, 981 | tag: options.tag 982 | }); 983 | 984 | const fromId = parseInt(options.from, 10); // Validation happens here 985 | const prompt = options.prompt; 986 | const useResearch = options.research || false; 987 | 988 | const tasksPath = taskMaster.getTasksPath(); 989 | 990 | // Resolve tag using standard pattern 991 | const tag = taskMaster.getCurrentTag(); 992 | 993 | // Show current tag context 994 | displayCurrentTagIndicator(tag); 995 | 996 | // Check if there's an 'id' option which is a common mistake (instead of 'from') 997 | if ( 998 | process.argv.includes('--id') || 999 | process.argv.some((arg) => arg.startsWith('--id=')) 1000 | ) { 1001 | console.error( 1002 | chalk.red('Error: The update command uses --from=<id>, not --id=<id>') 1003 | ); 1004 | console.log(chalk.yellow('\nTo update multiple tasks:')); 1005 | console.log( 1006 | ` task-master update --from=${fromId} --prompt="Your prompt here"` 1007 | ); 1008 | console.log( 1009 | chalk.yellow( 1010 | '\nTo update a single specific task, use the update-task command instead:' 1011 | ) 1012 | ); 1013 | console.log( 1014 | ` task-master update-task --id=<id> --prompt="Your prompt here"` 1015 | ); 1016 | process.exit(1); 1017 | } 1018 | 1019 | if (!prompt) { 1020 | console.error( 1021 | chalk.red( 1022 | 'Error: --prompt parameter is required. Please provide information about the changes.' 1023 | ) 1024 | ); 1025 | process.exit(1); 1026 | } 1027 | 1028 | console.log( 1029 | chalk.blue( 1030 | `Updating tasks from ID >= ${fromId} with prompt: "${prompt}"` 1031 | ) 1032 | ); 1033 | console.log(chalk.blue(`Tasks file: ${tasksPath}`)); 1034 | 1035 | if (useResearch) { 1036 | console.log( 1037 | chalk.blue('Using Perplexity AI for research-backed task updates') 1038 | ); 1039 | } 1040 | 1041 | // Call core updateTasks, passing context for CLI 1042 | await updateTasks( 1043 | taskMaster.getTasksPath(), 1044 | fromId, 1045 | prompt, 1046 | useResearch, 1047 | { projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag 1048 | ); 1049 | }); 1050 | 1051 | // update-task command 1052 | programInstance 1053 | .command('update-task') 1054 | .description( 1055 | 'Update a single specific task by ID with new information (use --id parameter)' 1056 | ) 1057 | .option( 1058 | '-f, --file <file>', 1059 | 'Path to the tasks file', 1060 | TASKMASTER_TASKS_FILE 1061 | ) 1062 | .option('-i, --id <id>', 'Task ID to update (required)') 1063 | .option( 1064 | '-p, --prompt <text>', 1065 | 'Prompt explaining the changes or new context (required)' 1066 | ) 1067 | .option( 1068 | '-r, --research', 1069 | 'Use Perplexity AI for research-backed task updates' 1070 | ) 1071 | .option( 1072 | '--append', 1073 | 'Append timestamped information to task details instead of full update' 1074 | ) 1075 | .option('--tag <tag>', 'Specify tag context for task operations') 1076 | .action(async (options) => { 1077 | try { 1078 | // Initialize TaskMaster 1079 | const taskMaster = initTaskMaster({ 1080 | tasksPath: options.file || true, 1081 | tag: options.tag 1082 | }); 1083 | const tasksPath = taskMaster.getTasksPath(); 1084 | 1085 | // Resolve tag using standard pattern 1086 | const tag = taskMaster.getCurrentTag(); 1087 | 1088 | // Show current tag context 1089 | displayCurrentTagIndicator(tag); 1090 | 1091 | // Validate required parameters 1092 | if (!options.id) { 1093 | console.error(chalk.red('Error: --id parameter is required')); 1094 | console.log( 1095 | chalk.yellow( 1096 | 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' 1097 | ) 1098 | ); 1099 | process.exit(1); 1100 | } 1101 | 1102 | // Parse the task ID and validate it's a number 1103 | const taskId = parseInt(options.id, 10); 1104 | if (Number.isNaN(taskId) || taskId <= 0) { 1105 | console.error( 1106 | chalk.red( 1107 | `Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.` 1108 | ) 1109 | ); 1110 | console.log( 1111 | chalk.yellow( 1112 | 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' 1113 | ) 1114 | ); 1115 | process.exit(1); 1116 | } 1117 | 1118 | if (!options.prompt) { 1119 | console.error( 1120 | chalk.red( 1121 | 'Error: --prompt parameter is required. Please provide information about the changes.' 1122 | ) 1123 | ); 1124 | console.log( 1125 | chalk.yellow( 1126 | 'Usage example: task-master update-task --id=23 --prompt="Update with new information"' 1127 | ) 1128 | ); 1129 | process.exit(1); 1130 | } 1131 | 1132 | const prompt = options.prompt; 1133 | const useResearch = options.research || false; 1134 | 1135 | // Validate tasks file exists 1136 | if (!fs.existsSync(tasksPath)) { 1137 | console.error( 1138 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 1139 | ); 1140 | if (tasksPath === TASKMASTER_TASKS_FILE) { 1141 | console.log( 1142 | chalk.yellow( 1143 | 'Hint: Run task-master init or task-master parse-prd to create tasks.json first' 1144 | ) 1145 | ); 1146 | } else { 1147 | console.log( 1148 | chalk.yellow( 1149 | `Hint: Check if the file path is correct: ${tasksPath}` 1150 | ) 1151 | ); 1152 | } 1153 | process.exit(1); 1154 | } 1155 | 1156 | console.log( 1157 | chalk.blue(`Updating task ${taskId} with prompt: "${prompt}"`) 1158 | ); 1159 | console.log(chalk.blue(`Tasks file: ${tasksPath}`)); 1160 | 1161 | if (useResearch) { 1162 | // Verify Perplexity API key exists if using research 1163 | if (!isApiKeySet('perplexity')) { 1164 | console.log( 1165 | chalk.yellow( 1166 | 'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.' 1167 | ) 1168 | ); 1169 | console.log( 1170 | chalk.yellow('Falling back to Claude AI for task update.') 1171 | ); 1172 | } else { 1173 | console.log( 1174 | chalk.blue('Using Perplexity AI for research-backed task update') 1175 | ); 1176 | } 1177 | } 1178 | 1179 | const result = await updateTaskById( 1180 | taskMaster.getTasksPath(), 1181 | taskId, 1182 | prompt, 1183 | useResearch, 1184 | { projectRoot: taskMaster.getProjectRoot(), tag }, 1185 | 'text', 1186 | options.append || false 1187 | ); 1188 | 1189 | // If the task wasn't updated (e.g., if it was already marked as done) 1190 | if (!result) { 1191 | console.log( 1192 | chalk.yellow( 1193 | '\nTask update was not completed. Review the messages above for details.' 1194 | ) 1195 | ); 1196 | } 1197 | } catch (error) { 1198 | console.error(chalk.red(`Error: ${error.message}`)); 1199 | 1200 | // Provide more helpful error messages for common issues 1201 | if ( 1202 | error.message.includes('task') && 1203 | error.message.includes('not found') 1204 | ) { 1205 | console.log(chalk.yellow('\nTo fix this issue:')); 1206 | console.log( 1207 | ' 1. Run task-master list to see all available task IDs' 1208 | ); 1209 | console.log(' 2. Use a valid task ID with the --id parameter'); 1210 | } else if (error.message.includes('API key')) { 1211 | console.log( 1212 | chalk.yellow( 1213 | '\nThis error is related to API keys. Check your environment variables.' 1214 | ) 1215 | ); 1216 | } 1217 | 1218 | // Use getDebugFlag getter instead of CONFIG.debug 1219 | if (getDebugFlag()) { 1220 | console.error(error); 1221 | } 1222 | 1223 | process.exit(1); 1224 | } 1225 | }); 1226 | 1227 | // update-subtask command 1228 | programInstance 1229 | .command('update-subtask') 1230 | .description( 1231 | 'Update a subtask by appending additional timestamped information' 1232 | ) 1233 | .option( 1234 | '-f, --file <file>', 1235 | 'Path to the tasks file', 1236 | TASKMASTER_TASKS_FILE 1237 | ) 1238 | .option( 1239 | '-i, --id <id>', 1240 | 'Subtask ID to update in format "parentId.subtaskId" (required)' 1241 | ) 1242 | .option( 1243 | '-p, --prompt <text>', 1244 | 'Prompt explaining what information to add (required)' 1245 | ) 1246 | .option('-r, --research', 'Use Perplexity AI for research-backed updates') 1247 | .option('--tag <tag>', 'Specify tag context for task operations') 1248 | .action(async (options) => { 1249 | try { 1250 | // Initialize TaskMaster 1251 | const taskMaster = initTaskMaster({ 1252 | tasksPath: options.file || true, 1253 | tag: options.tag 1254 | }); 1255 | const tasksPath = taskMaster.getTasksPath(); 1256 | 1257 | // Resolve tag using standard pattern 1258 | const tag = taskMaster.getCurrentTag(); 1259 | 1260 | // Show current tag context 1261 | displayCurrentTagIndicator(tag); 1262 | 1263 | // Validate required parameters 1264 | if (!options.id) { 1265 | console.error(chalk.red('Error: --id parameter is required')); 1266 | console.log( 1267 | chalk.yellow( 1268 | 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' 1269 | ) 1270 | ); 1271 | process.exit(1); 1272 | } 1273 | 1274 | // Validate subtask ID format (should contain a dot) 1275 | const subtaskId = options.id; 1276 | if (!subtaskId.includes('.')) { 1277 | console.error( 1278 | chalk.red( 1279 | `Error: Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"` 1280 | ) 1281 | ); 1282 | console.log( 1283 | chalk.yellow( 1284 | 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' 1285 | ) 1286 | ); 1287 | process.exit(1); 1288 | } 1289 | 1290 | if (!options.prompt) { 1291 | console.error( 1292 | chalk.red( 1293 | 'Error: --prompt parameter is required. Please provide information to add to the subtask.' 1294 | ) 1295 | ); 1296 | console.log( 1297 | chalk.yellow( 1298 | 'Usage example: task-master update-subtask --id=5.2 --prompt="Add more details about the API endpoint"' 1299 | ) 1300 | ); 1301 | process.exit(1); 1302 | } 1303 | 1304 | const prompt = options.prompt; 1305 | const useResearch = options.research || false; 1306 | 1307 | // Validate tasks file exists 1308 | if (!fs.existsSync(tasksPath)) { 1309 | console.error( 1310 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 1311 | ); 1312 | if (tasksPath === TASKMASTER_TASKS_FILE) { 1313 | console.log( 1314 | chalk.yellow( 1315 | 'Hint: Run task-master init or task-master parse-prd to create tasks.json first' 1316 | ) 1317 | ); 1318 | } else { 1319 | console.log( 1320 | chalk.yellow( 1321 | `Hint: Check if the file path is correct: ${tasksPath}` 1322 | ) 1323 | ); 1324 | } 1325 | process.exit(1); 1326 | } 1327 | 1328 | console.log( 1329 | chalk.blue(`Updating subtask ${subtaskId} with prompt: "${prompt}"`) 1330 | ); 1331 | console.log(chalk.blue(`Tasks file: ${tasksPath}`)); 1332 | 1333 | if (useResearch) { 1334 | // Verify Perplexity API key exists if using research 1335 | if (!isApiKeySet('perplexity')) { 1336 | console.log( 1337 | chalk.yellow( 1338 | 'Warning: PERPLEXITY_API_KEY environment variable is missing. Research-backed updates will not be available.' 1339 | ) 1340 | ); 1341 | console.log( 1342 | chalk.yellow('Falling back to Claude AI for subtask update.') 1343 | ); 1344 | } else { 1345 | console.log( 1346 | chalk.blue( 1347 | 'Using Perplexity AI for research-backed subtask update' 1348 | ) 1349 | ); 1350 | } 1351 | } 1352 | 1353 | const result = await updateSubtaskById( 1354 | taskMaster.getTasksPath(), 1355 | subtaskId, 1356 | prompt, 1357 | useResearch, 1358 | { projectRoot: taskMaster.getProjectRoot(), tag } 1359 | ); 1360 | 1361 | if (!result) { 1362 | console.log( 1363 | chalk.yellow( 1364 | '\nSubtask update was not completed. Review the messages above for details.' 1365 | ) 1366 | ); 1367 | } 1368 | } catch (error) { 1369 | console.error(chalk.red(`Error: ${error.message}`)); 1370 | 1371 | // Provide more helpful error messages for common issues 1372 | if ( 1373 | error.message.includes('subtask') && 1374 | error.message.includes('not found') 1375 | ) { 1376 | console.log(chalk.yellow('\nTo fix this issue:')); 1377 | console.log( 1378 | ' 1. Run task-master list --with-subtasks to see all available subtask IDs' 1379 | ); 1380 | console.log( 1381 | ' 2. Use a valid subtask ID with the --id parameter in format "parentId.subtaskId"' 1382 | ); 1383 | } else if (error.message.includes('API key')) { 1384 | console.log( 1385 | chalk.yellow( 1386 | '\nThis error is related to API keys. Check your environment variables.' 1387 | ) 1388 | ); 1389 | } 1390 | 1391 | // Use getDebugFlag getter instead of CONFIG.debug 1392 | if (getDebugFlag()) { 1393 | console.error(error); 1394 | } 1395 | 1396 | process.exit(1); 1397 | } 1398 | }); 1399 | 1400 | // scope-up command 1401 | programInstance 1402 | .command('scope-up') 1403 | .description('Increase task complexity with AI assistance') 1404 | .option( 1405 | '-f, --file <file>', 1406 | 'Path to the tasks file', 1407 | TASKMASTER_TASKS_FILE 1408 | ) 1409 | .option( 1410 | '-i, --id <ids>', 1411 | 'Comma-separated task/subtask IDs to scope up (required)' 1412 | ) 1413 | .option( 1414 | '-s, --strength <level>', 1415 | 'Complexity increase strength: light, regular, heavy', 1416 | 'regular' 1417 | ) 1418 | .option( 1419 | '-p, --prompt <text>', 1420 | 'Custom instructions for targeted scope adjustments' 1421 | ) 1422 | .option('-r, --research', 'Use research AI for more informed adjustments') 1423 | .option('--tag <tag>', 'Specify tag context for task operations') 1424 | .action(async (options) => { 1425 | try { 1426 | // Initialize TaskMaster 1427 | const taskMaster = initTaskMaster({ 1428 | tasksPath: options.file || true, 1429 | tag: options.tag 1430 | }); 1431 | const tasksPath = taskMaster.getTasksPath(); 1432 | const tag = taskMaster.getCurrentTag(); 1433 | 1434 | // Show current tag context 1435 | displayCurrentTagIndicator(tag); 1436 | 1437 | // Validate required parameters 1438 | if (!options.id) { 1439 | console.error(chalk.red('Error: --id parameter is required')); 1440 | console.log( 1441 | chalk.yellow( 1442 | 'Usage example: task-master scope-up --id=1,2,3 --strength=regular' 1443 | ) 1444 | ); 1445 | process.exit(1); 1446 | } 1447 | 1448 | // Parse and validate task IDs 1449 | const taskIds = options.id.split(',').map((id) => { 1450 | const parsed = parseInt(id.trim(), 10); 1451 | if (Number.isNaN(parsed) || parsed <= 0) { 1452 | console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`)); 1453 | process.exit(1); 1454 | } 1455 | return parsed; 1456 | }); 1457 | 1458 | // Validate strength level 1459 | if (!validateStrength(options.strength)) { 1460 | console.error( 1461 | chalk.red( 1462 | `Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy` 1463 | ) 1464 | ); 1465 | process.exit(1); 1466 | } 1467 | 1468 | // Validate tasks file exists 1469 | if (!fs.existsSync(tasksPath)) { 1470 | console.error( 1471 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 1472 | ); 1473 | process.exit(1); 1474 | } 1475 | 1476 | console.log( 1477 | chalk.blue( 1478 | `Scoping up ${taskIds.length} task(s): ${taskIds.join(', ')}` 1479 | ) 1480 | ); 1481 | console.log(chalk.blue(`Strength level: ${options.strength}`)); 1482 | if (options.prompt) { 1483 | console.log(chalk.blue(`Custom instructions: ${options.prompt}`)); 1484 | } 1485 | 1486 | const context = { 1487 | projectRoot: taskMaster.getProjectRoot(), 1488 | tag, 1489 | commandName: 'scope-up', 1490 | outputType: 'cli', 1491 | research: options.research || false 1492 | }; 1493 | 1494 | const result = await scopeUpTask( 1495 | tasksPath, 1496 | taskIds, 1497 | options.strength, 1498 | options.prompt || null, 1499 | context, 1500 | 'text' 1501 | ); 1502 | 1503 | console.log( 1504 | chalk.green( 1505 | `✅ Successfully scoped up ${result.updatedTasks.length} task(s)` 1506 | ) 1507 | ); 1508 | } catch (error) { 1509 | console.error(chalk.red(`Error: ${error.message}`)); 1510 | 1511 | if (error.message.includes('not found')) { 1512 | console.log(chalk.yellow('\nTo fix this issue:')); 1513 | console.log( 1514 | ' 1. Run task-master list to see all available task IDs' 1515 | ); 1516 | console.log(' 2. Use valid task IDs with the --id parameter'); 1517 | } 1518 | 1519 | if (getDebugFlag()) { 1520 | console.error(error); 1521 | } 1522 | 1523 | process.exit(1); 1524 | } 1525 | }); 1526 | 1527 | // scope-down command 1528 | programInstance 1529 | .command('scope-down') 1530 | .description('Decrease task complexity with AI assistance') 1531 | .option( 1532 | '-f, --file <file>', 1533 | 'Path to the tasks file', 1534 | TASKMASTER_TASKS_FILE 1535 | ) 1536 | .option( 1537 | '-i, --id <ids>', 1538 | 'Comma-separated task/subtask IDs to scope down (required)' 1539 | ) 1540 | .option( 1541 | '-s, --strength <level>', 1542 | 'Complexity decrease strength: light, regular, heavy', 1543 | 'regular' 1544 | ) 1545 | .option( 1546 | '-p, --prompt <text>', 1547 | 'Custom instructions for targeted scope adjustments' 1548 | ) 1549 | .option('-r, --research', 'Use research AI for more informed adjustments') 1550 | .option('--tag <tag>', 'Specify tag context for task operations') 1551 | .action(async (options) => { 1552 | try { 1553 | // Initialize TaskMaster 1554 | const taskMaster = initTaskMaster({ 1555 | tasksPath: options.file || true, 1556 | tag: options.tag 1557 | }); 1558 | const tasksPath = taskMaster.getTasksPath(); 1559 | const tag = taskMaster.getCurrentTag(); 1560 | 1561 | // Show current tag context 1562 | displayCurrentTagIndicator(tag); 1563 | 1564 | // Validate required parameters 1565 | if (!options.id) { 1566 | console.error(chalk.red('Error: --id parameter is required')); 1567 | console.log( 1568 | chalk.yellow( 1569 | 'Usage example: task-master scope-down --id=1,2,3 --strength=regular' 1570 | ) 1571 | ); 1572 | process.exit(1); 1573 | } 1574 | 1575 | // Parse and validate task IDs 1576 | const taskIds = options.id.split(',').map((id) => { 1577 | const parsed = parseInt(id.trim(), 10); 1578 | if (Number.isNaN(parsed) || parsed <= 0) { 1579 | console.error(chalk.red(`Error: Invalid task ID: ${id.trim()}`)); 1580 | process.exit(1); 1581 | } 1582 | return parsed; 1583 | }); 1584 | 1585 | // Validate strength level 1586 | if (!validateStrength(options.strength)) { 1587 | console.error( 1588 | chalk.red( 1589 | `Error: Invalid strength level: ${options.strength}. Must be one of: light, regular, heavy` 1590 | ) 1591 | ); 1592 | process.exit(1); 1593 | } 1594 | 1595 | // Validate tasks file exists 1596 | if (!fs.existsSync(tasksPath)) { 1597 | console.error( 1598 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 1599 | ); 1600 | process.exit(1); 1601 | } 1602 | 1603 | console.log( 1604 | chalk.blue( 1605 | `Scoping down ${taskIds.length} task(s): ${taskIds.join(', ')}` 1606 | ) 1607 | ); 1608 | console.log(chalk.blue(`Strength level: ${options.strength}`)); 1609 | if (options.prompt) { 1610 | console.log(chalk.blue(`Custom instructions: ${options.prompt}`)); 1611 | } 1612 | 1613 | const context = { 1614 | projectRoot: taskMaster.getProjectRoot(), 1615 | tag, 1616 | commandName: 'scope-down', 1617 | outputType: 'cli', 1618 | research: options.research || false 1619 | }; 1620 | 1621 | const result = await scopeDownTask( 1622 | tasksPath, 1623 | taskIds, 1624 | options.strength, 1625 | options.prompt || null, 1626 | context, 1627 | 'text' 1628 | ); 1629 | 1630 | console.log( 1631 | chalk.green( 1632 | `✅ Successfully scoped down ${result.updatedTasks.length} task(s)` 1633 | ) 1634 | ); 1635 | } catch (error) { 1636 | console.error(chalk.red(`Error: ${error.message}`)); 1637 | 1638 | if (error.message.includes('not found')) { 1639 | console.log(chalk.yellow('\nTo fix this issue:')); 1640 | console.log( 1641 | ' 1. Run task-master list to see all available task IDs' 1642 | ); 1643 | console.log(' 2. Use valid task IDs with the --id parameter'); 1644 | } 1645 | 1646 | if (getDebugFlag()) { 1647 | console.error(error); 1648 | } 1649 | 1650 | process.exit(1); 1651 | } 1652 | }); 1653 | 1654 | // generate command 1655 | programInstance 1656 | .command('generate') 1657 | .description('Generate task files from tasks.json') 1658 | .option( 1659 | '-f, --file <file>', 1660 | 'Path to the tasks file', 1661 | TASKMASTER_TASKS_FILE 1662 | ) 1663 | .option( 1664 | '-o, --output <dir>', 1665 | 'Output directory', 1666 | path.dirname(TASKMASTER_TASKS_FILE) 1667 | ) 1668 | .option('--tag <tag>', 'Specify tag context for task operations') 1669 | .action(async (options) => { 1670 | // Initialize TaskMaster 1671 | const taskMaster = initTaskMaster({ 1672 | tasksPath: options.file || true, 1673 | tag: options.tag 1674 | }); 1675 | 1676 | const outputDir = options.output; 1677 | const tag = taskMaster.getCurrentTag(); 1678 | 1679 | console.log( 1680 | chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`) 1681 | ); 1682 | console.log(chalk.blue(`Output directory: ${outputDir}`)); 1683 | 1684 | await generateTaskFiles(taskMaster.getTasksPath(), outputDir, { 1685 | projectRoot: taskMaster.getProjectRoot(), 1686 | tag 1687 | }); 1688 | }); 1689 | 1690 | // Register the set-status command from @tm/cli 1691 | // Handles task status updates with proper error handling and validation 1692 | SetStatusCommand.registerOn(programInstance); 1693 | 1694 | // NEW: Register the new list command from @tm/cli 1695 | // This command handles all its own configuration and logic 1696 | ListTasksCommand.registerOn(programInstance); 1697 | 1698 | // Register the auth command from @tm/cli 1699 | // Handles authentication with tryhamster.com 1700 | AuthCommand.registerOn(programInstance); 1701 | 1702 | // Register the context command from @tm/cli 1703 | // Manages workspace context (org/brief selection) 1704 | ContextCommand.registerOn(programInstance); 1705 | 1706 | // Register the show command from @tm/cli 1707 | // Displays detailed information about tasks 1708 | ShowCommand.registerOn(programInstance); 1709 | 1710 | // Register the start command from @tm/cli 1711 | // Starts working on a task by launching claude-code with a standardized prompt 1712 | StartCommand.registerOn(programInstance); 1713 | 1714 | // expand command 1715 | programInstance 1716 | .command('expand') 1717 | .description('Expand a task into subtasks using AI') 1718 | .option('-i, --id <id>', 'ID of the task to expand') 1719 | .option( 1720 | '-a, --all', 1721 | 'Expand all pending tasks based on complexity analysis' 1722 | ) 1723 | .option( 1724 | '-n, --num <number>', 1725 | 'Number of subtasks to generate (uses complexity analysis by default if available)' 1726 | ) 1727 | .option( 1728 | '-r, --research', 1729 | 'Enable research-backed generation (e.g., using Perplexity)', 1730 | false 1731 | ) 1732 | .option('-p, --prompt <text>', 'Additional context for subtask generation') 1733 | .option('-f, --force', 'Force expansion even if subtasks exist', false) // Ensure force option exists 1734 | .option( 1735 | '--file <file>', 1736 | 'Path to the tasks file (relative to project root)', 1737 | TASKMASTER_TASKS_FILE // Allow file override 1738 | ) // Allow file override 1739 | .option( 1740 | '-cr, --complexity-report <file>', 1741 | 'Path to the complexity report file (use this to specify the complexity report, not --file)' 1742 | // Removed default value to allow tag-specific auto-detection 1743 | ) 1744 | .option('--tag <tag>', 'Specify tag context for task operations') 1745 | .action(async (options) => { 1746 | // Initialize TaskMaster 1747 | const initOptions = { 1748 | tasksPath: options.file || true, 1749 | tag: options.tag 1750 | }; 1751 | 1752 | if (options.complexityReport) { 1753 | initOptions.complexityReportPath = options.complexityReport; 1754 | } 1755 | 1756 | const taskMaster = initTaskMaster(initOptions); 1757 | 1758 | const tag = taskMaster.getCurrentTag(); 1759 | 1760 | // Show current tag context 1761 | displayCurrentTagIndicator(tag); 1762 | 1763 | if (options.all) { 1764 | // --- Handle expand --all --- 1765 | console.log(chalk.blue('Expanding all pending tasks...')); 1766 | // Updated call to the refactored expandAllTasks 1767 | try { 1768 | const result = await expandAllTasks( 1769 | taskMaster.getTasksPath(), 1770 | options.num, // Pass num 1771 | options.research, // Pass research flag 1772 | options.prompt, // Pass additional context 1773 | options.force, // Pass force flag 1774 | { 1775 | projectRoot: taskMaster.getProjectRoot(), 1776 | tag, 1777 | complexityReportPath: taskMaster.getComplexityReportPath() 1778 | } // Pass context with projectRoot and tag 1779 | // outputFormat defaults to 'text' in expandAllTasks for CLI 1780 | ); 1781 | } catch (error) { 1782 | console.error( 1783 | chalk.red(`Error expanding all tasks: ${error.message}`) 1784 | ); 1785 | process.exit(1); 1786 | } 1787 | } else if (options.id) { 1788 | // --- Handle expand --id <id> (Should be correct from previous refactor) --- 1789 | if (!options.id) { 1790 | console.error( 1791 | chalk.red('Error: Task ID is required unless using --all.') 1792 | ); 1793 | process.exit(1); 1794 | } 1795 | 1796 | console.log(chalk.blue(`Expanding task ${options.id}...`)); 1797 | try { 1798 | // Call the refactored expandTask function 1799 | await expandTask( 1800 | taskMaster.getTasksPath(), 1801 | options.id, 1802 | options.num, 1803 | options.research, 1804 | options.prompt, 1805 | { 1806 | projectRoot: taskMaster.getProjectRoot(), 1807 | tag, 1808 | complexityReportPath: taskMaster.getComplexityReportPath() 1809 | }, // Pass context with projectRoot and tag 1810 | options.force // Pass the force flag down 1811 | ); 1812 | // expandTask logs its own success/failure for single task 1813 | } catch (error) { 1814 | console.error( 1815 | chalk.red(`Error expanding task ${options.id}: ${error.message}`) 1816 | ); 1817 | process.exit(1); 1818 | } 1819 | } else { 1820 | console.error( 1821 | chalk.red('Error: You must specify either a task ID (--id) or --all.') 1822 | ); 1823 | programInstance.help(); // Show help 1824 | } 1825 | }); 1826 | 1827 | // analyze-complexity command 1828 | programInstance 1829 | .command('analyze-complexity') 1830 | .description( 1831 | `Analyze tasks and generate expansion recommendations${chalk.reset('')}` 1832 | ) 1833 | .option('-o, --output <file>', 'Output file path for the report') 1834 | .option( 1835 | '-m, --model <model>', 1836 | 'LLM model to use for analysis (defaults to configured model)' 1837 | ) 1838 | .option( 1839 | '-t, --threshold <number>', 1840 | 'Minimum complexity score to recommend expansion (1-10)', 1841 | '5' 1842 | ) 1843 | .option( 1844 | '-f, --file <file>', 1845 | 'Path to the tasks file', 1846 | TASKMASTER_TASKS_FILE 1847 | ) 1848 | .option( 1849 | '-r, --research', 1850 | 'Use Perplexity AI for research-backed complexity analysis' 1851 | ) 1852 | .option( 1853 | '-i, --id <ids>', 1854 | 'Comma-separated list of specific task IDs to analyze (e.g., "1,3,5")' 1855 | ) 1856 | .option('--from <id>', 'Starting task ID in a range to analyze') 1857 | .option('--to <id>', 'Ending task ID in a range to analyze') 1858 | .option('--tag <tag>', 'Specify tag context for task operations') 1859 | .action(async (options) => { 1860 | // Initialize TaskMaster 1861 | const initOptions = { 1862 | tasksPath: options.file || true, // Tasks file is required to analyze 1863 | tag: options.tag 1864 | }; 1865 | // Only include complexityReportPath if output is explicitly specified 1866 | if (options.output) { 1867 | initOptions.complexityReportPath = options.output; 1868 | } 1869 | 1870 | const taskMaster = initTaskMaster(initOptions); 1871 | 1872 | const modelOverride = options.model; 1873 | const thresholdScore = parseFloat(options.threshold); 1874 | const useResearch = options.research || false; 1875 | 1876 | // Use the provided tag, or the current active tag, or default to 'master' 1877 | const targetTag = taskMaster.getCurrentTag(); 1878 | 1879 | // Show current tag context 1880 | displayCurrentTagIndicator(targetTag); 1881 | 1882 | // Use user's explicit output path if provided, otherwise use tag-aware default 1883 | const outputPath = taskMaster.getComplexityReportPath(); 1884 | 1885 | console.log( 1886 | chalk.blue( 1887 | `Analyzing task complexity from: ${taskMaster.getTasksPath()}` 1888 | ) 1889 | ); 1890 | console.log(chalk.blue(`Output report will be saved to: ${outputPath}`)); 1891 | 1892 | if (options.id) { 1893 | console.log(chalk.blue(`Analyzing specific task IDs: ${options.id}`)); 1894 | } else if (options.from || options.to) { 1895 | const fromStr = options.from ? options.from : 'first'; 1896 | const toStr = options.to ? options.to : 'last'; 1897 | console.log( 1898 | chalk.blue(`Analyzing tasks in range: ${fromStr} to ${toStr}`) 1899 | ); 1900 | } 1901 | 1902 | if (useResearch) { 1903 | console.log( 1904 | chalk.blue( 1905 | 'Using Perplexity AI for research-backed complexity analysis' 1906 | ) 1907 | ); 1908 | } 1909 | 1910 | // Update options with tag-aware output path and context 1911 | const updatedOptions = { 1912 | ...options, 1913 | output: outputPath, 1914 | tag: targetTag, 1915 | projectRoot: taskMaster.getProjectRoot(), 1916 | file: taskMaster.getTasksPath() 1917 | }; 1918 | 1919 | await analyzeTaskComplexity(updatedOptions); 1920 | }); 1921 | 1922 | // research command 1923 | programInstance 1924 | .command('research') 1925 | .description('Perform AI-powered research queries with project context') 1926 | .argument('[prompt]', 'Research prompt to investigate') 1927 | .option('--file <file>', 'Path to the tasks file') 1928 | .option( 1929 | '-i, --id <ids>', 1930 | 'Comma-separated task/subtask IDs to include as context (e.g., "15,16.2")' 1931 | ) 1932 | .option( 1933 | '-f, --files <paths>', 1934 | 'Comma-separated file paths to include as context' 1935 | ) 1936 | .option( 1937 | '-c, --context <text>', 1938 | 'Additional custom context to include in the research prompt' 1939 | ) 1940 | .option( 1941 | '-t, --tree', 1942 | 'Include project file tree structure in the research context' 1943 | ) 1944 | .option( 1945 | '-s, --save <file>', 1946 | 'Save research results to the specified task/subtask(s)' 1947 | ) 1948 | .option( 1949 | '-d, --detail <level>', 1950 | 'Output detail level: low, medium, high', 1951 | 'medium' 1952 | ) 1953 | .option( 1954 | '--save-to <id>', 1955 | 'Automatically save research results to specified task/subtask ID (e.g., "15" or "15.2")' 1956 | ) 1957 | .option( 1958 | '--save-file', 1959 | 'Save research results to .taskmaster/docs/research/ directory' 1960 | ) 1961 | .option('--tag <tag>', 'Specify tag context for task operations') 1962 | .action(async (prompt, options) => { 1963 | // Initialize TaskMaster 1964 | const initOptions = { 1965 | tasksPath: options.file || true, 1966 | tag: options.tag 1967 | }; 1968 | 1969 | const taskMaster = initTaskMaster(initOptions); 1970 | 1971 | // Parameter validation 1972 | if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) { 1973 | console.error( 1974 | chalk.red('Error: Research prompt is required and cannot be empty') 1975 | ); 1976 | showResearchHelp(); 1977 | process.exit(1); 1978 | } 1979 | 1980 | // Validate detail level 1981 | const validDetailLevels = ['low', 'medium', 'high']; 1982 | if ( 1983 | options.detail && 1984 | !validDetailLevels.includes(options.detail.toLowerCase()) 1985 | ) { 1986 | console.error( 1987 | chalk.red( 1988 | `Error: Detail level must be one of: ${validDetailLevels.join(', ')}` 1989 | ) 1990 | ); 1991 | process.exit(1); 1992 | } 1993 | 1994 | // Validate and parse task IDs if provided 1995 | let taskIds = []; 1996 | if (options.id) { 1997 | try { 1998 | taskIds = options.id.split(',').map((id) => { 1999 | const trimmedId = id.trim(); 2000 | // Support both task IDs (e.g., "15") and subtask IDs (e.g., "15.2") 2001 | if (!/^\d+(\.\d+)?$/.test(trimmedId)) { 2002 | throw new Error( 2003 | `Invalid task ID format: "${trimmedId}". Expected format: "15" or "15.2"` 2004 | ); 2005 | } 2006 | return trimmedId; 2007 | }); 2008 | } catch (error) { 2009 | console.error(chalk.red(`Error parsing task IDs: ${error.message}`)); 2010 | process.exit(1); 2011 | } 2012 | } 2013 | 2014 | // Validate and parse file paths if provided 2015 | let filePaths = []; 2016 | if (options.files) { 2017 | try { 2018 | filePaths = options.files.split(',').map((filePath) => { 2019 | const trimmedPath = filePath.trim(); 2020 | if (trimmedPath.length === 0) { 2021 | throw new Error('Empty file path provided'); 2022 | } 2023 | return trimmedPath; 2024 | }); 2025 | } catch (error) { 2026 | console.error( 2027 | chalk.red(`Error parsing file paths: ${error.message}`) 2028 | ); 2029 | process.exit(1); 2030 | } 2031 | } 2032 | 2033 | // Validate save-to option if provided 2034 | if (options.saveTo) { 2035 | const saveToId = options.saveTo.trim(); 2036 | if (saveToId.length === 0) { 2037 | console.error(chalk.red('Error: Save-to ID cannot be empty')); 2038 | process.exit(1); 2039 | } 2040 | // Validate ID format: number or number.number 2041 | if (!/^\d+(\.\d+)?$/.test(saveToId)) { 2042 | console.error( 2043 | chalk.red( 2044 | 'Error: Save-to ID must be in format "15" for task or "15.2" for subtask' 2045 | ) 2046 | ); 2047 | process.exit(1); 2048 | } 2049 | } 2050 | 2051 | // Validate save option if provided (legacy file save) 2052 | if (options.save) { 2053 | const saveTarget = options.save.trim(); 2054 | if (saveTarget.length === 0) { 2055 | console.error(chalk.red('Error: Save target cannot be empty')); 2056 | process.exit(1); 2057 | } 2058 | // Check if it's a valid file path (basic validation) 2059 | if (saveTarget.includes('..') || saveTarget.startsWith('/')) { 2060 | console.error( 2061 | chalk.red( 2062 | 'Error: Save path must be relative and cannot contain ".."' 2063 | ) 2064 | ); 2065 | process.exit(1); 2066 | } 2067 | } 2068 | 2069 | const tag = taskMaster.getCurrentTag(); 2070 | 2071 | // Show current tag context 2072 | displayCurrentTagIndicator(tag); 2073 | 2074 | // Validate tasks file exists if task IDs are specified 2075 | if (taskIds.length > 0) { 2076 | try { 2077 | const tasksData = readJSON( 2078 | taskMaster.getTasksPath(), 2079 | taskMaster.getProjectRoot(), 2080 | tag 2081 | ); 2082 | if (!tasksData || !tasksData.tasks) { 2083 | console.error( 2084 | chalk.red( 2085 | `Error: No valid tasks found in ${taskMaster.getTasksPath()} for tag '${tag}'` 2086 | ) 2087 | ); 2088 | process.exit(1); 2089 | } 2090 | } catch (error) { 2091 | console.error( 2092 | chalk.red(`Error reading tasks file: ${error.message}`) 2093 | ); 2094 | process.exit(1); 2095 | } 2096 | } 2097 | 2098 | // Validate file paths exist if specified 2099 | if (filePaths.length > 0) { 2100 | for (const filePath of filePaths) { 2101 | const fullPath = path.isAbsolute(filePath) 2102 | ? filePath 2103 | : path.join(taskMaster.getProjectRoot(), filePath); 2104 | if (!fs.existsSync(fullPath)) { 2105 | console.error(chalk.red(`Error: File not found: ${filePath}`)); 2106 | process.exit(1); 2107 | } 2108 | } 2109 | } 2110 | 2111 | // Create validated parameters object 2112 | const validatedParams = { 2113 | prompt: prompt.trim(), 2114 | taskIds: taskIds, 2115 | filePaths: filePaths, 2116 | customContext: options.context ? options.context.trim() : null, 2117 | includeProjectTree: !!options.tree, 2118 | saveTarget: options.save ? options.save.trim() : null, 2119 | saveToId: options.saveTo ? options.saveTo.trim() : null, 2120 | allowFollowUp: true, // Always allow follow-up in CLI 2121 | detailLevel: options.detail ? options.detail.toLowerCase() : 'medium', 2122 | tasksPath: taskMaster.getTasksPath(), 2123 | projectRoot: taskMaster.getProjectRoot() 2124 | }; 2125 | 2126 | // Display what we're about to do 2127 | console.log(chalk.blue(`Researching: "${validatedParams.prompt}"`)); 2128 | 2129 | if (validatedParams.taskIds.length > 0) { 2130 | console.log( 2131 | chalk.gray(`Task context: ${validatedParams.taskIds.join(', ')}`) 2132 | ); 2133 | } 2134 | 2135 | if (validatedParams.filePaths.length > 0) { 2136 | console.log( 2137 | chalk.gray(`File context: ${validatedParams.filePaths.join(', ')}`) 2138 | ); 2139 | } 2140 | 2141 | if (validatedParams.customContext) { 2142 | console.log( 2143 | chalk.gray( 2144 | `Custom context: ${validatedParams.customContext.substring(0, 50)}${validatedParams.customContext.length > 50 ? '...' : ''}` 2145 | ) 2146 | ); 2147 | } 2148 | 2149 | if (validatedParams.includeProjectTree) { 2150 | console.log(chalk.gray('Including project file tree')); 2151 | } 2152 | 2153 | console.log(chalk.gray(`Detail level: ${validatedParams.detailLevel}`)); 2154 | 2155 | try { 2156 | // Import the research function 2157 | const { performResearch } = await import('./task-manager/research.js'); 2158 | 2159 | // Prepare research options 2160 | const researchOptions = { 2161 | taskIds: validatedParams.taskIds, 2162 | filePaths: validatedParams.filePaths, 2163 | customContext: validatedParams.customContext || '', 2164 | includeProjectTree: validatedParams.includeProjectTree, 2165 | detailLevel: validatedParams.detailLevel, 2166 | projectRoot: validatedParams.projectRoot, 2167 | saveToFile: !!options.saveFile, 2168 | tag: tag 2169 | }; 2170 | 2171 | // Execute research 2172 | const result = await performResearch( 2173 | validatedParams.prompt, 2174 | researchOptions, 2175 | { 2176 | commandName: 'research', 2177 | outputType: 'cli', 2178 | tag: tag 2179 | }, 2180 | 'text', 2181 | validatedParams.allowFollowUp // Pass follow-up flag 2182 | ); 2183 | 2184 | // Auto-save to task/subtask if requested and no interactive save occurred 2185 | if (validatedParams.saveToId && !result.interactiveSaveOccurred) { 2186 | try { 2187 | const isSubtask = validatedParams.saveToId.includes('.'); 2188 | 2189 | // Format research content for saving 2190 | const researchContent = `## Research Query: ${validatedParams.prompt} 2191 | 2192 | **Detail Level:** ${result.detailLevel} 2193 | **Context Size:** ${result.contextSize} characters 2194 | **Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()} 2195 | 2196 | ### Results 2197 | 2198 | ${result.result}`; 2199 | 2200 | if (isSubtask) { 2201 | // Save to subtask 2202 | const { updateSubtaskById } = await import( 2203 | './task-manager/update-subtask-by-id.js' 2204 | ); 2205 | 2206 | await updateSubtaskById( 2207 | validatedParams.tasksPath, 2208 | validatedParams.saveToId, 2209 | researchContent, 2210 | false, // useResearch = false for simple append 2211 | { 2212 | commandName: 'research-save', 2213 | outputType: 'cli', 2214 | projectRoot: validatedParams.projectRoot, 2215 | tag: tag 2216 | }, 2217 | 'text' 2218 | ); 2219 | 2220 | console.log( 2221 | chalk.green( 2222 | `✅ Research saved to subtask ${validatedParams.saveToId}` 2223 | ) 2224 | ); 2225 | } else { 2226 | // Save to task 2227 | const updateTaskById = ( 2228 | await import('./task-manager/update-task-by-id.js') 2229 | ).default; 2230 | 2231 | const taskIdNum = parseInt(validatedParams.saveToId, 10); 2232 | await updateTaskById( 2233 | validatedParams.tasksPath, 2234 | taskIdNum, 2235 | researchContent, 2236 | false, // useResearch = false for simple append 2237 | { 2238 | commandName: 'research-save', 2239 | outputType: 'cli', 2240 | projectRoot: validatedParams.projectRoot, 2241 | tag: tag 2242 | }, 2243 | 'text', 2244 | true // appendMode = true 2245 | ); 2246 | 2247 | console.log( 2248 | chalk.green( 2249 | `✅ Research saved to task ${validatedParams.saveToId}` 2250 | ) 2251 | ); 2252 | } 2253 | } catch (saveError) { 2254 | console.log( 2255 | chalk.red(`❌ Error saving to task/subtask: ${saveError.message}`) 2256 | ); 2257 | } 2258 | } 2259 | 2260 | // Save results to file if requested (legacy) 2261 | if (validatedParams.saveTarget) { 2262 | const saveContent = `# Research Query: ${validatedParams.prompt} 2263 | 2264 | **Detail Level:** ${result.detailLevel} 2265 | **Context Size:** ${result.contextSize} characters 2266 | **Timestamp:** ${new Date().toISOString()} 2267 | 2268 | ## Results 2269 | 2270 | ${result.result} 2271 | `; 2272 | 2273 | fs.writeFileSync(validatedParams.saveTarget, saveContent, 'utf-8'); 2274 | console.log( 2275 | chalk.green(`\n💾 Results saved to: ${validatedParams.saveTarget}`) 2276 | ); 2277 | } 2278 | } catch (error) { 2279 | console.error(chalk.red(`\n❌ Research failed: ${error.message}`)); 2280 | process.exit(1); 2281 | } 2282 | }); 2283 | 2284 | // clear-subtasks command 2285 | programInstance 2286 | .command('clear-subtasks') 2287 | .description('Clear subtasks from specified tasks') 2288 | .option( 2289 | '-f, --file <file>', 2290 | 'Path to the tasks file', 2291 | TASKMASTER_TASKS_FILE 2292 | ) 2293 | .option( 2294 | '-i, --id <ids>', 2295 | 'Task IDs (comma-separated) to clear subtasks from' 2296 | ) 2297 | .option('--all', 'Clear subtasks from all tasks') 2298 | .option('--tag <tag>', 'Specify tag context for task operations') 2299 | .action(async (options) => { 2300 | const taskIds = options.id; 2301 | const all = options.all; 2302 | 2303 | // Initialize TaskMaster 2304 | const taskMaster = initTaskMaster({ 2305 | tasksPath: options.file || true, 2306 | tag: options.tag 2307 | }); 2308 | 2309 | const tag = taskMaster.getCurrentTag(); 2310 | 2311 | // Show current tag context 2312 | displayCurrentTagIndicator(tag); 2313 | 2314 | if (!taskIds && !all) { 2315 | console.error( 2316 | chalk.red( 2317 | 'Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks' 2318 | ) 2319 | ); 2320 | process.exit(1); 2321 | } 2322 | 2323 | if (all) { 2324 | // If --all is specified, get all task IDs 2325 | const data = readJSON( 2326 | taskMaster.getTasksPath(), 2327 | taskMaster.getProjectRoot(), 2328 | tag 2329 | ); 2330 | if (!data || !data.tasks) { 2331 | console.error(chalk.red('Error: No valid tasks found')); 2332 | process.exit(1); 2333 | } 2334 | const allIds = data.tasks.map((t) => t.id).join(','); 2335 | clearSubtasks(taskMaster.getTasksPath(), allIds, { 2336 | projectRoot: taskMaster.getProjectRoot(), 2337 | tag 2338 | }); 2339 | } else { 2340 | clearSubtasks(taskMaster.getTasksPath(), taskIds, { 2341 | projectRoot: taskMaster.getProjectRoot(), 2342 | tag 2343 | }); 2344 | } 2345 | }); 2346 | 2347 | // add-task command 2348 | programInstance 2349 | .command('add-task') 2350 | .description('Add a new task using AI, optionally providing manual details') 2351 | .option( 2352 | '-f, --file <file>', 2353 | 'Path to the tasks file', 2354 | TASKMASTER_TASKS_FILE 2355 | ) 2356 | .option( 2357 | '-p, --prompt <prompt>', 2358 | 'Description of the task to add (required if not using manual fields)' 2359 | ) 2360 | .option('-t, --title <title>', 'Task title (for manual task creation)') 2361 | .option( 2362 | '-d, --description <description>', 2363 | 'Task description (for manual task creation)' 2364 | ) 2365 | .option( 2366 | '--details <details>', 2367 | 'Implementation details (for manual task creation)' 2368 | ) 2369 | .option( 2370 | '--dependencies <dependencies>', 2371 | 'Comma-separated list of task IDs this task depends on' 2372 | ) 2373 | .option( 2374 | '--priority <priority>', 2375 | 'Task priority (high, medium, low)', 2376 | 'medium' 2377 | ) 2378 | .option( 2379 | '-r, --research', 2380 | 'Whether to use research capabilities for task creation' 2381 | ) 2382 | .option('--tag <tag>', 'Specify tag context for task operations') 2383 | .action(async (options) => { 2384 | const isManualCreation = options.title && options.description; 2385 | 2386 | // Validate that either prompt or title+description are provided 2387 | if (!options.prompt && !isManualCreation) { 2388 | console.error( 2389 | chalk.red( 2390 | 'Error: Either --prompt or both --title and --description must be provided' 2391 | ) 2392 | ); 2393 | process.exit(1); 2394 | } 2395 | 2396 | const tasksPath = options.file || TASKMASTER_TASKS_FILE; 2397 | 2398 | if (!fs.existsSync(tasksPath)) { 2399 | console.error( 2400 | `❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file at ${TASKMASTER_TASKS_FILE}` 2401 | ); 2402 | process.exit(1); 2403 | } 2404 | 2405 | // Correctly determine projectRoot 2406 | // Initialize TaskMaster 2407 | const taskMaster = initTaskMaster({ 2408 | tasksPath: options.file || true, 2409 | tag: options.tag 2410 | }); 2411 | 2412 | const projectRoot = taskMaster.getProjectRoot(); 2413 | 2414 | const tag = taskMaster.getCurrentTag(); 2415 | 2416 | // Show current tag context 2417 | displayCurrentTagIndicator(tag); 2418 | 2419 | let manualTaskData = null; 2420 | if (isManualCreation) { 2421 | manualTaskData = { 2422 | title: options.title, 2423 | description: options.description, 2424 | details: options.details || '', 2425 | testStrategy: options.testStrategy || '' 2426 | }; 2427 | // Restore specific logging for manual creation 2428 | console.log( 2429 | chalk.blue(`Creating task manually with title: "${options.title}"`) 2430 | ); 2431 | } else { 2432 | // Restore specific logging for AI creation 2433 | console.log( 2434 | chalk.blue(`Creating task with AI using prompt: "${options.prompt}"`) 2435 | ); 2436 | } 2437 | 2438 | // Log dependencies and priority if provided (restored) 2439 | const dependenciesArray = options.dependencies 2440 | ? options.dependencies.split(',').map((id) => id.trim()) 2441 | : []; 2442 | if (dependenciesArray.length > 0) { 2443 | console.log( 2444 | chalk.blue(`Dependencies: [${dependenciesArray.join(', ')}]`) 2445 | ); 2446 | } 2447 | if (options.priority) { 2448 | console.log(chalk.blue(`Priority: ${options.priority}`)); 2449 | } 2450 | 2451 | const context = { 2452 | projectRoot, 2453 | tag, 2454 | commandName: 'add-task', 2455 | outputType: 'cli' 2456 | }; 2457 | 2458 | try { 2459 | const { newTaskId, telemetryData } = await addTask( 2460 | taskMaster.getTasksPath(), 2461 | options.prompt, 2462 | dependenciesArray, 2463 | options.priority, 2464 | context, 2465 | 'text', 2466 | manualTaskData, 2467 | options.research 2468 | ); 2469 | 2470 | // addTask handles detailed CLI success logging AND telemetry display when outputFormat is 'text' 2471 | // No need to call displayAiUsageSummary here anymore. 2472 | } catch (error) { 2473 | console.error(chalk.red(`Error adding task: ${error.message}`)); 2474 | if (error.details) { 2475 | console.error(chalk.red(error.details)); 2476 | } 2477 | process.exit(1); 2478 | } 2479 | }); 2480 | 2481 | // next command 2482 | programInstance 2483 | .command('next') 2484 | .description( 2485 | `Show the next task to work on based on dependencies and status${chalk.reset('')}` 2486 | ) 2487 | .option( 2488 | '-f, --file <file>', 2489 | 'Path to the tasks file', 2490 | TASKMASTER_TASKS_FILE 2491 | ) 2492 | .option( 2493 | '-r, --report <report>', 2494 | 'Path to the complexity report file', 2495 | COMPLEXITY_REPORT_FILE 2496 | ) 2497 | .option('--tag <tag>', 'Specify tag context for task operations') 2498 | .action(async (options) => { 2499 | const initOptions = { 2500 | tasksPath: options.file || true, 2501 | tag: options.tag 2502 | }; 2503 | 2504 | if (options.report && options.report !== COMPLEXITY_REPORT_FILE) { 2505 | initOptions.complexityReportPath = options.report; 2506 | } 2507 | 2508 | // Initialize TaskMaster 2509 | const taskMaster = initTaskMaster({ 2510 | tasksPath: options.file || true, 2511 | tag: options.tag, 2512 | complexityReportPath: options.report || false 2513 | }); 2514 | 2515 | const tag = taskMaster.getCurrentTag(); 2516 | 2517 | const context = { 2518 | projectRoot: taskMaster.getProjectRoot(), 2519 | tag 2520 | }; 2521 | 2522 | // Show current tag context 2523 | displayCurrentTagIndicator(tag); 2524 | 2525 | await displayNextTask( 2526 | taskMaster.getTasksPath(), 2527 | taskMaster.getComplexityReportPath(), 2528 | context 2529 | ); 2530 | }); 2531 | 2532 | // add-dependency command 2533 | programInstance 2534 | .command('add-dependency') 2535 | .description('Add a dependency to a task') 2536 | .option('-i, --id <id>', 'Task ID to add dependency to') 2537 | .option('-d, --depends-on <id>', 'Task ID that will become a dependency') 2538 | .option( 2539 | '-f, --file <file>', 2540 | 'Path to the tasks file', 2541 | TASKMASTER_TASKS_FILE 2542 | ) 2543 | .option('--tag <tag>', 'Specify tag context for task operations') 2544 | .action(async (options) => { 2545 | const initOptions = { 2546 | tasksPath: options.file || true, 2547 | tag: options.tag 2548 | }; 2549 | 2550 | // Initialize TaskMaster 2551 | const taskMaster = initTaskMaster(initOptions); 2552 | 2553 | const taskId = options.id; 2554 | const dependencyId = options.dependsOn; 2555 | 2556 | // Resolve tag using standard pattern 2557 | const tag = taskMaster.getCurrentTag(); 2558 | 2559 | // Show current tag context 2560 | displayCurrentTagIndicator(tag); 2561 | 2562 | if (!taskId || !dependencyId) { 2563 | console.error( 2564 | chalk.red('Error: Both --id and --depends-on are required') 2565 | ); 2566 | process.exit(1); 2567 | } 2568 | 2569 | // Handle subtask IDs correctly by preserving the string format for IDs containing dots 2570 | // Only use parseInt for simple numeric IDs 2571 | const formattedTaskId = taskId.includes('.') 2572 | ? taskId 2573 | : parseInt(taskId, 10); 2574 | const formattedDependencyId = dependencyId.includes('.') 2575 | ? dependencyId 2576 | : parseInt(dependencyId, 10); 2577 | 2578 | await addDependency( 2579 | taskMaster.getTasksPath(), 2580 | formattedTaskId, 2581 | formattedDependencyId, 2582 | { 2583 | projectRoot: taskMaster.getProjectRoot(), 2584 | tag 2585 | } 2586 | ); 2587 | }); 2588 | 2589 | // remove-dependency command 2590 | programInstance 2591 | .command('remove-dependency') 2592 | .description('Remove a dependency from a task') 2593 | .option('-i, --id <id>', 'Task ID to remove dependency from') 2594 | .option('-d, --depends-on <id>', 'Task ID to remove as a dependency') 2595 | .option( 2596 | '-f, --file <file>', 2597 | 'Path to the tasks file', 2598 | TASKMASTER_TASKS_FILE 2599 | ) 2600 | .option('--tag <tag>', 'Specify tag context for task operations') 2601 | .action(async (options) => { 2602 | const initOptions = { 2603 | tasksPath: options.file || true, 2604 | tag: options.tag 2605 | }; 2606 | 2607 | // Initialize TaskMaster 2608 | const taskMaster = initTaskMaster(initOptions); 2609 | 2610 | const taskId = options.id; 2611 | const dependencyId = options.dependsOn; 2612 | 2613 | // Resolve tag using standard pattern 2614 | const tag = taskMaster.getCurrentTag(); 2615 | 2616 | // Show current tag context 2617 | displayCurrentTagIndicator(tag); 2618 | 2619 | if (!taskId || !dependencyId) { 2620 | console.error( 2621 | chalk.red('Error: Both --id and --depends-on are required') 2622 | ); 2623 | process.exit(1); 2624 | } 2625 | 2626 | // Handle subtask IDs correctly by preserving the string format for IDs containing dots 2627 | // Only use parseInt for simple numeric IDs 2628 | const formattedTaskId = taskId.includes('.') 2629 | ? taskId 2630 | : parseInt(taskId, 10); 2631 | const formattedDependencyId = dependencyId.includes('.') 2632 | ? dependencyId 2633 | : parseInt(dependencyId, 10); 2634 | 2635 | await removeDependency( 2636 | taskMaster.getTasksPath(), 2637 | formattedTaskId, 2638 | formattedDependencyId, 2639 | { 2640 | projectRoot: taskMaster.getProjectRoot(), 2641 | tag 2642 | } 2643 | ); 2644 | }); 2645 | 2646 | // validate-dependencies command 2647 | programInstance 2648 | .command('validate-dependencies') 2649 | .description( 2650 | `Identify invalid dependencies without fixing them${chalk.reset('')}` 2651 | ) 2652 | .option( 2653 | '-f, --file <file>', 2654 | 'Path to the tasks file', 2655 | TASKMASTER_TASKS_FILE 2656 | ) 2657 | .option('--tag <tag>', 'Specify tag context for task operations') 2658 | .action(async (options) => { 2659 | const initOptions = { 2660 | tasksPath: options.file || true, 2661 | tag: options.tag 2662 | }; 2663 | 2664 | // Initialize TaskMaster 2665 | const taskMaster = initTaskMaster(initOptions); 2666 | 2667 | // Resolve tag using standard pattern 2668 | const tag = taskMaster.getCurrentTag(); 2669 | 2670 | // Show current tag context 2671 | displayCurrentTagIndicator(tag); 2672 | 2673 | await validateDependenciesCommand(taskMaster.getTasksPath(), { 2674 | context: { projectRoot: taskMaster.getProjectRoot(), tag } 2675 | }); 2676 | }); 2677 | 2678 | // fix-dependencies command 2679 | programInstance 2680 | .command('fix-dependencies') 2681 | .description(`Fix invalid dependencies automatically${chalk.reset('')}`) 2682 | .option( 2683 | '-f, --file <file>', 2684 | 'Path to the tasks file', 2685 | TASKMASTER_TASKS_FILE 2686 | ) 2687 | .option('--tag <tag>', 'Specify tag context for task operations') 2688 | .action(async (options) => { 2689 | const initOptions = { 2690 | tasksPath: options.file || true, 2691 | tag: options.tag 2692 | }; 2693 | 2694 | // Initialize TaskMaster 2695 | const taskMaster = initTaskMaster(initOptions); 2696 | 2697 | // Resolve tag using standard pattern 2698 | const tag = taskMaster.getCurrentTag(); 2699 | 2700 | // Show current tag context 2701 | displayCurrentTagIndicator(tag); 2702 | 2703 | await fixDependenciesCommand(taskMaster.getTasksPath(), { 2704 | context: { projectRoot: taskMaster.getProjectRoot(), tag } 2705 | }); 2706 | }); 2707 | 2708 | // complexity-report command 2709 | programInstance 2710 | .command('complexity-report') 2711 | .description(`Display the complexity analysis report${chalk.reset('')}`) 2712 | .option( 2713 | '-f, --file <file>', 2714 | 'Path to the report file', 2715 | COMPLEXITY_REPORT_FILE 2716 | ) 2717 | .option('--tag <tag>', 'Specify tag context for task operations') 2718 | .action(async (options) => { 2719 | const initOptions = { 2720 | tag: options.tag 2721 | }; 2722 | 2723 | if (options.file && options.file !== COMPLEXITY_REPORT_FILE) { 2724 | initOptions.complexityReportPath = options.file; 2725 | } 2726 | 2727 | // Initialize TaskMaster 2728 | const taskMaster = initTaskMaster(initOptions); 2729 | 2730 | // Show current tag context 2731 | displayCurrentTagIndicator(taskMaster.getCurrentTag()); 2732 | 2733 | await displayComplexityReport(taskMaster.getComplexityReportPath()); 2734 | }); 2735 | 2736 | // add-subtask command 2737 | programInstance 2738 | .command('add-subtask') 2739 | .description('Add a subtask to an existing task') 2740 | .option( 2741 | '-f, --file <file>', 2742 | 'Path to the tasks file', 2743 | TASKMASTER_TASKS_FILE 2744 | ) 2745 | .option('-p, --parent <id>', 'Parent task ID (required)') 2746 | .option('-i, --task-id <id>', 'Existing task ID to convert to subtask') 2747 | .option( 2748 | '-t, --title <title>', 2749 | 'Title for the new subtask (when creating a new subtask)' 2750 | ) 2751 | .option('-d, --description <text>', 'Description for the new subtask') 2752 | .option('--details <text>', 'Implementation details for the new subtask') 2753 | .option( 2754 | '--dependencies <ids>', 2755 | 'Comma-separated list of dependency IDs for the new subtask' 2756 | ) 2757 | .option('-s, --status <status>', 'Status for the new subtask', 'pending') 2758 | .option('--generate', 'Regenerate task files after adding subtask') 2759 | .option('--tag <tag>', 'Specify tag context for task operations') 2760 | .action(async (options) => { 2761 | // Initialize TaskMaster 2762 | const taskMaster = initTaskMaster({ 2763 | tasksPath: options.file || true, 2764 | tag: options.tag 2765 | }); 2766 | 2767 | const parentId = options.parent; 2768 | const existingTaskId = options.taskId; 2769 | const generateFiles = options.generate || false; 2770 | 2771 | // Resolve tag using standard pattern 2772 | const tag = taskMaster.getCurrentTag(); 2773 | 2774 | // Show current tag context 2775 | displayCurrentTagIndicator(tag); 2776 | 2777 | if (!parentId) { 2778 | console.error( 2779 | chalk.red( 2780 | 'Error: --parent parameter is required. Please provide a parent task ID.' 2781 | ) 2782 | ); 2783 | showAddSubtaskHelp(); 2784 | process.exit(1); 2785 | } 2786 | 2787 | // Parse dependencies if provided 2788 | let dependencies = []; 2789 | if (options.dependencies) { 2790 | dependencies = options.dependencies.split(',').map((id) => { 2791 | // Handle both regular IDs and dot notation 2792 | return id.includes('.') ? id.trim() : parseInt(id.trim(), 10); 2793 | }); 2794 | } 2795 | 2796 | try { 2797 | if (existingTaskId) { 2798 | // Convert existing task to subtask 2799 | console.log( 2800 | chalk.blue( 2801 | `Converting task ${existingTaskId} to a subtask of ${parentId}...` 2802 | ) 2803 | ); 2804 | await addSubtask( 2805 | taskMaster.getTasksPath(), 2806 | parentId, 2807 | existingTaskId, 2808 | null, 2809 | generateFiles, 2810 | { projectRoot: taskMaster.getProjectRoot(), tag } 2811 | ); 2812 | console.log( 2813 | chalk.green( 2814 | `✓ Task ${existingTaskId} successfully converted to a subtask of task ${parentId}` 2815 | ) 2816 | ); 2817 | } else if (options.title) { 2818 | // Create new subtask with provided data 2819 | console.log( 2820 | chalk.blue(`Creating new subtask for parent task ${parentId}...`) 2821 | ); 2822 | 2823 | const newSubtaskData = { 2824 | title: options.title, 2825 | description: options.description || '', 2826 | details: options.details || '', 2827 | status: options.status || 'pending', 2828 | dependencies: dependencies 2829 | }; 2830 | 2831 | const subtask = await addSubtask( 2832 | taskMaster.getTasksPath(), 2833 | parentId, 2834 | null, 2835 | newSubtaskData, 2836 | generateFiles, 2837 | { projectRoot: taskMaster.getProjectRoot(), tag } 2838 | ); 2839 | console.log( 2840 | chalk.green( 2841 | `✓ New subtask ${parentId}.${subtask.id} successfully created` 2842 | ) 2843 | ); 2844 | 2845 | // Display success message and suggested next steps 2846 | console.log( 2847 | boxen( 2848 | chalk.white.bold( 2849 | `Subtask ${parentId}.${subtask.id} Added Successfully` 2850 | ) + 2851 | '\n\n' + 2852 | chalk.white(`Title: ${subtask.title}`) + 2853 | '\n' + 2854 | chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + 2855 | '\n' + 2856 | (dependencies.length > 0 2857 | ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + 2858 | '\n' 2859 | : '') + 2860 | '\n' + 2861 | chalk.white.bold('Next Steps:') + 2862 | '\n' + 2863 | chalk.cyan( 2864 | `1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks` 2865 | ) + 2866 | '\n' + 2867 | chalk.cyan( 2868 | `2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it` 2869 | ), 2870 | { 2871 | padding: 1, 2872 | borderColor: 'green', 2873 | borderStyle: 'round', 2874 | margin: { top: 1 } 2875 | } 2876 | ) 2877 | ); 2878 | } else { 2879 | console.error( 2880 | chalk.red('Error: Either --task-id or --title must be provided.') 2881 | ); 2882 | console.log( 2883 | boxen( 2884 | chalk.white.bold('Usage Examples:') + 2885 | '\n\n' + 2886 | chalk.white('Convert existing task to subtask:') + 2887 | '\n' + 2888 | chalk.yellow( 2889 | ` task-master add-subtask --parent=5 --task-id=8` 2890 | ) + 2891 | '\n\n' + 2892 | chalk.white('Create new subtask:') + 2893 | '\n' + 2894 | chalk.yellow( 2895 | ` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"` 2896 | ) + 2897 | '\n\n', 2898 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 2899 | ) 2900 | ); 2901 | process.exit(1); 2902 | } 2903 | } catch (error) { 2904 | console.error(chalk.red(`Error: ${error.message}`)); 2905 | showAddSubtaskHelp(); 2906 | process.exit(1); 2907 | } 2908 | }) 2909 | .on('error', function (err) { 2910 | console.error(chalk.red(`Error: ${err.message}`)); 2911 | showAddSubtaskHelp(); 2912 | process.exit(1); 2913 | }); 2914 | 2915 | // Helper function to show add-subtask command help 2916 | function showAddSubtaskHelp() { 2917 | console.log( 2918 | boxen( 2919 | `${chalk.white.bold('Add Subtask Command Help')}\n\n${chalk.cyan('Usage:')}\n task-master add-subtask --parent=<id> [options]\n\n${chalk.cyan('Options:')}\n -p, --parent <id> Parent task ID (required)\n -i, --task-id <id> Existing task ID to convert to subtask\n -t, --title <title> Title for the new subtask\n -d, --description <text> Description for the new subtask\n --details <text> Implementation details for the new subtask\n --dependencies <ids> Comma-separated list of dependency IDs\n -s, --status <status> Status for the new subtask (default: "pending")\n -f, --file <file> Path to the tasks file (default: "${TASKMASTER_TASKS_FILE}")\n --generate Regenerate task files after adding subtask\n\n${chalk.cyan('Examples:')}\n task-master add-subtask --parent=5 --task-id=8\n task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form" --generate`, 2920 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 2921 | ) 2922 | ); 2923 | } 2924 | 2925 | // remove-subtask command 2926 | programInstance 2927 | .command('remove-subtask') 2928 | .description('Remove a subtask from its parent task') 2929 | .option( 2930 | '-f, --file <file>', 2931 | 'Path to the tasks file', 2932 | TASKMASTER_TASKS_FILE 2933 | ) 2934 | .option( 2935 | '-i, --id <id>', 2936 | 'Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)' 2937 | ) 2938 | .option( 2939 | '-c, --convert', 2940 | 'Convert the subtask to a standalone task instead of deleting it' 2941 | ) 2942 | .option('--generate', 'Regenerate task files after removing subtask') 2943 | .option('--tag <tag>', 'Specify tag context for task operations') 2944 | .action(async (options) => { 2945 | // Initialize TaskMaster 2946 | const taskMaster = initTaskMaster({ 2947 | tasksPath: options.file || true, 2948 | tag: options.tag 2949 | }); 2950 | 2951 | const subtaskIds = options.id; 2952 | const convertToTask = options.convert || false; 2953 | const generateFiles = options.generate || false; 2954 | const tag = taskMaster.getCurrentTag(); 2955 | 2956 | if (!subtaskIds) { 2957 | console.error( 2958 | chalk.red( 2959 | 'Error: --id parameter is required. Please provide subtask ID(s) in format "parentId.subtaskId".' 2960 | ) 2961 | ); 2962 | showRemoveSubtaskHelp(); 2963 | process.exit(1); 2964 | } 2965 | 2966 | try { 2967 | // Split by comma to support multiple subtask IDs 2968 | const subtaskIdArray = subtaskIds.split(',').map((id) => id.trim()); 2969 | 2970 | for (const subtaskId of subtaskIdArray) { 2971 | // Validate subtask ID format 2972 | if (!subtaskId.includes('.')) { 2973 | console.error( 2974 | chalk.red( 2975 | `Error: Subtask ID "${subtaskId}" must be in format "parentId.subtaskId"` 2976 | ) 2977 | ); 2978 | showRemoveSubtaskHelp(); 2979 | process.exit(1); 2980 | } 2981 | 2982 | console.log(chalk.blue(`Removing subtask ${subtaskId}...`)); 2983 | if (convertToTask) { 2984 | console.log( 2985 | chalk.blue('The subtask will be converted to a standalone task') 2986 | ); 2987 | } 2988 | 2989 | const result = await removeSubtask( 2990 | taskMaster.getTasksPath(), 2991 | subtaskId, 2992 | convertToTask, 2993 | generateFiles, 2994 | { projectRoot: taskMaster.getProjectRoot(), tag } 2995 | ); 2996 | 2997 | if (convertToTask && result) { 2998 | // Display success message and next steps for converted task 2999 | console.log( 3000 | boxen( 3001 | chalk.white.bold( 3002 | `Subtask ${subtaskId} Converted to Task #${result.id}` 3003 | ) + 3004 | '\n\n' + 3005 | chalk.white(`Title: ${result.title}`) + 3006 | '\n' + 3007 | chalk.white(`Status: ${getStatusWithColor(result.status)}`) + 3008 | '\n' + 3009 | chalk.white( 3010 | `Dependencies: ${result.dependencies.join(', ')}` 3011 | ) + 3012 | '\n\n' + 3013 | chalk.white.bold('Next Steps:') + 3014 | '\n' + 3015 | chalk.cyan( 3016 | `1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task` 3017 | ) + 3018 | '\n' + 3019 | chalk.cyan( 3020 | `2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it` 3021 | ), 3022 | { 3023 | padding: 1, 3024 | borderColor: 'green', 3025 | borderStyle: 'round', 3026 | margin: { top: 1 } 3027 | } 3028 | ) 3029 | ); 3030 | } else { 3031 | // Display success message for deleted subtask 3032 | console.log( 3033 | boxen( 3034 | chalk.white.bold(`Subtask ${subtaskId} Removed`) + 3035 | '\n\n' + 3036 | chalk.white('The subtask has been successfully deleted.'), 3037 | { 3038 | padding: 1, 3039 | borderColor: 'green', 3040 | borderStyle: 'round', 3041 | margin: { top: 1 } 3042 | } 3043 | ) 3044 | ); 3045 | } 3046 | } 3047 | } catch (error) { 3048 | console.error(chalk.red(`Error: ${error.message}`)); 3049 | showRemoveSubtaskHelp(); 3050 | process.exit(1); 3051 | } 3052 | }) 3053 | .on('error', function (err) { 3054 | console.error(chalk.red(`Error: ${err.message}`)); 3055 | showRemoveSubtaskHelp(); 3056 | process.exit(1); 3057 | }); 3058 | 3059 | // Helper function to show remove-subtask command help 3060 | function showRemoveSubtaskHelp() { 3061 | console.log( 3062 | boxen( 3063 | chalk.white.bold('Remove Subtask Command Help') + 3064 | '\n\n' + 3065 | chalk.cyan('Usage:') + 3066 | '\n' + 3067 | ` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` + 3068 | chalk.cyan('Options:') + 3069 | '\n' + 3070 | ' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' + 3071 | ' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' + 3072 | ' -f, --file <file> Path to the tasks file (default: "' + 3073 | TASKMASTER_TASKS_FILE + 3074 | '")\n' + 3075 | ' --skip-generate Skip regenerating task files\n\n' + 3076 | chalk.cyan('Examples:') + 3077 | '\n' + 3078 | ' task-master remove-subtask --id=5.2\n' + 3079 | ' task-master remove-subtask --id=5.2,6.3,7.1\n' + 3080 | ' task-master remove-subtask --id=5.2 --convert', 3081 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3082 | ) 3083 | ); 3084 | } 3085 | 3086 | // Helper function to show tags command help 3087 | function showTagsHelp() { 3088 | console.log( 3089 | boxen( 3090 | chalk.white.bold('Tags Command Help') + 3091 | '\n\n' + 3092 | chalk.cyan('Usage:') + 3093 | '\n' + 3094 | ` task-master tags [options]\n\n` + 3095 | chalk.cyan('Options:') + 3096 | '\n' + 3097 | ' -f, --file <file> Path to the tasks file (default: "' + 3098 | TASKMASTER_TASKS_FILE + 3099 | '")\n' + 3100 | ' --show-metadata Show detailed metadata for each tag\n\n' + 3101 | chalk.cyan('Examples:') + 3102 | '\n' + 3103 | ' task-master tags\n' + 3104 | ' task-master tags --show-metadata\n\n' + 3105 | chalk.cyan('Related Commands:') + 3106 | '\n' + 3107 | ' task-master add-tag <name> Create a new tag\n' + 3108 | ' task-master use-tag <name> Switch to a tag\n' + 3109 | ' task-master delete-tag <name> Delete a tag', 3110 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3111 | ) 3112 | ); 3113 | } 3114 | 3115 | // Helper function to show add-tag command help 3116 | function showAddTagHelp() { 3117 | console.log( 3118 | boxen( 3119 | chalk.white.bold('Add Tag Command Help') + 3120 | '\n\n' + 3121 | chalk.cyan('Usage:') + 3122 | '\n' + 3123 | ` task-master add-tag <tagName> [options]\n\n` + 3124 | chalk.cyan('Options:') + 3125 | '\n' + 3126 | ' -f, --file <file> Path to the tasks file (default: "' + 3127 | TASKMASTER_TASKS_FILE + 3128 | '")\n' + 3129 | ' --copy-from-current Copy tasks from the current tag to the new tag\n' + 3130 | ' --copy-from <tag> Copy tasks from the specified tag to the new tag\n' + 3131 | ' -d, --description <text> Optional description for the tag\n\n' + 3132 | chalk.cyan('Examples:') + 3133 | '\n' + 3134 | ' task-master add-tag feature-xyz\n' + 3135 | ' task-master add-tag feature-xyz --copy-from-current\n' + 3136 | ' task-master add-tag feature-xyz --copy-from master\n' + 3137 | ' task-master add-tag feature-xyz -d "Feature XYZ development"', 3138 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3139 | ) 3140 | ); 3141 | } 3142 | 3143 | // Helper function to show delete-tag command help 3144 | function showDeleteTagHelp() { 3145 | console.log( 3146 | boxen( 3147 | chalk.white.bold('Delete Tag Command Help') + 3148 | '\n\n' + 3149 | chalk.cyan('Usage:') + 3150 | '\n' + 3151 | ` task-master delete-tag <tagName> [options]\n\n` + 3152 | chalk.cyan('Options:') + 3153 | '\n' + 3154 | ' -f, --file <file> Path to the tasks file (default: "' + 3155 | TASKMASTER_TASKS_FILE + 3156 | '")\n' + 3157 | ' -y, --yes Skip confirmation prompts\n\n' + 3158 | chalk.cyan('Examples:') + 3159 | '\n' + 3160 | ' task-master delete-tag feature-xyz\n' + 3161 | ' task-master delete-tag feature-xyz --yes\n\n' + 3162 | chalk.yellow('Warning:') + 3163 | '\n' + 3164 | ' This will permanently delete the tag and all its tasks!', 3165 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3166 | ) 3167 | ); 3168 | } 3169 | 3170 | // Helper function to show use-tag command help 3171 | function showUseTagHelp() { 3172 | console.log( 3173 | boxen( 3174 | chalk.white.bold('Use Tag Command Help') + 3175 | '\n\n' + 3176 | chalk.cyan('Usage:') + 3177 | '\n' + 3178 | ` task-master use-tag <tagName> [options]\n\n` + 3179 | chalk.cyan('Options:') + 3180 | '\n' + 3181 | ' -f, --file <file> Path to the tasks file (default: "' + 3182 | TASKMASTER_TASKS_FILE + 3183 | '")\n\n' + 3184 | chalk.cyan('Examples:') + 3185 | '\n' + 3186 | ' task-master use-tag feature-xyz\n' + 3187 | ' task-master use-tag master\n\n' + 3188 | chalk.cyan('Related Commands:') + 3189 | '\n' + 3190 | ' task-master tags List all available tags\n' + 3191 | ' task-master add-tag <name> Create a new tag', 3192 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3193 | ) 3194 | ); 3195 | } 3196 | 3197 | // Helper function to show research command help 3198 | function showResearchHelp() { 3199 | console.log( 3200 | boxen( 3201 | chalk.white.bold('Research Command Help') + 3202 | '\n\n' + 3203 | chalk.cyan('Usage:') + 3204 | '\n' + 3205 | ` task-master research "<query>" [options]\n\n` + 3206 | chalk.cyan('Required:') + 3207 | '\n' + 3208 | ' <query> Research question or prompt (required)\n\n' + 3209 | chalk.cyan('Context Options:') + 3210 | '\n' + 3211 | ' -i, --id <ids> Comma-separated task/subtask IDs for context (e.g., "15,23.2")\n' + 3212 | ' -f, --files <paths> Comma-separated file paths for context\n' + 3213 | ' -c, --context <text> Additional custom context text\n' + 3214 | ' --tree Include project file tree structure\n\n' + 3215 | chalk.cyan('Output Options:') + 3216 | '\n' + 3217 | ' -d, --detail <level> Detail level: low, medium, high (default: medium)\n' + 3218 | ' --save-to <id> Auto-save results to task/subtask ID (e.g., "15" or "15.2")\n' + 3219 | ' --tag <tag> Specify tag context for task operations\n\n' + 3220 | chalk.cyan('Examples:') + 3221 | '\n' + 3222 | ' task-master research "How should I implement user authentication?"\n' + 3223 | ' task-master research "What\'s the best approach?" --id=15,23.2\n' + 3224 | ' task-master research "How does auth work?" --files=src/auth.js --tree\n' + 3225 | ' task-master research "Implementation steps?" --save-to=15.2 --detail=high', 3226 | { padding: 1, borderColor: 'blue', borderStyle: 'round' } 3227 | ) 3228 | ); 3229 | } 3230 | 3231 | // remove-task command 3232 | programInstance 3233 | .command('remove-task') 3234 | .description('Remove one or more tasks or subtasks permanently') 3235 | .option( 3236 | '-i, --id <ids>', 3237 | 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")' 3238 | ) 3239 | .option( 3240 | '-f, --file <file>', 3241 | 'Path to the tasks file', 3242 | TASKMASTER_TASKS_FILE 3243 | ) 3244 | .option('-y, --yes', 'Skip confirmation prompt', false) 3245 | .option('--tag <tag>', 'Specify tag context for task operations') 3246 | .action(async (options) => { 3247 | // Initialize TaskMaster 3248 | const taskMaster = initTaskMaster({ 3249 | tasksPath: options.file || true, 3250 | tag: options.tag 3251 | }); 3252 | 3253 | const taskIdsString = options.id; 3254 | 3255 | // Resolve tag using standard pattern 3256 | const tag = taskMaster.getCurrentTag(); 3257 | 3258 | // Show current tag context 3259 | displayCurrentTagIndicator(tag); 3260 | 3261 | if (!taskIdsString) { 3262 | console.error(chalk.red('Error: Task ID(s) are required')); 3263 | console.error( 3264 | chalk.yellow( 3265 | 'Usage: task-master remove-task --id=<taskId1,taskId2...>' 3266 | ) 3267 | ); 3268 | process.exit(1); 3269 | } 3270 | 3271 | const taskIdsToRemove = taskIdsString 3272 | .split(',') 3273 | .map((id) => id.trim()) 3274 | .filter(Boolean); 3275 | 3276 | if (taskIdsToRemove.length === 0) { 3277 | console.error(chalk.red('Error: No valid task IDs provided.')); 3278 | process.exit(1); 3279 | } 3280 | 3281 | try { 3282 | // Read data once for checks and confirmation 3283 | const data = readJSON( 3284 | taskMaster.getTasksPath(), 3285 | taskMaster.getProjectRoot(), 3286 | tag 3287 | ); 3288 | if (!data || !data.tasks) { 3289 | console.error( 3290 | chalk.red(`Error: No valid tasks found in ${tasksPath}`) 3291 | ); 3292 | process.exit(1); 3293 | } 3294 | 3295 | const existingTasksToRemove = []; 3296 | const nonExistentIds = []; 3297 | let totalSubtasksToDelete = 0; 3298 | const dependentTaskMessages = []; 3299 | 3300 | for (const taskId of taskIdsToRemove) { 3301 | if (!taskExists(data.tasks, taskId)) { 3302 | nonExistentIds.push(taskId); 3303 | } else { 3304 | // Correctly extract the task object from the result of findTaskById 3305 | const findResult = findTaskById(data.tasks, taskId); 3306 | const taskObject = findResult.task; // Get the actual task/subtask object 3307 | 3308 | if (taskObject) { 3309 | existingTasksToRemove.push({ id: taskId, task: taskObject }); // Push the actual task object 3310 | 3311 | // If it's a main task, count its subtasks and check dependents 3312 | if (!taskObject.isSubtask) { 3313 | // Check the actual task object 3314 | if (taskObject.subtasks && taskObject.subtasks.length > 0) { 3315 | totalSubtasksToDelete += taskObject.subtasks.length; 3316 | } 3317 | const dependentTasks = data.tasks.filter( 3318 | (t) => 3319 | t.dependencies && 3320 | t.dependencies.includes(parseInt(taskId, 10)) 3321 | ); 3322 | if (dependentTasks.length > 0) { 3323 | dependentTaskMessages.push( 3324 | ` - Task ${taskId}: ${dependentTasks.length} dependent tasks (${dependentTasks.map((t) => t.id).join(', ')})` 3325 | ); 3326 | } 3327 | } 3328 | } else { 3329 | // Handle case where findTaskById returned null for the task property (should be rare) 3330 | nonExistentIds.push(`${taskId} (error finding details)`); 3331 | } 3332 | } 3333 | } 3334 | 3335 | if (nonExistentIds.length > 0) { 3336 | console.warn( 3337 | chalk.yellow( 3338 | `Warning: The following task IDs were not found: ${nonExistentIds.join(', ')}` 3339 | ) 3340 | ); 3341 | } 3342 | 3343 | if (existingTasksToRemove.length === 0) { 3344 | console.log(chalk.blue('No existing tasks found to remove.')); 3345 | process.exit(0); 3346 | } 3347 | 3348 | // Skip confirmation if --yes flag is provided 3349 | if (!options.yes) { 3350 | console.log(); 3351 | console.log( 3352 | chalk.red.bold( 3353 | `⚠️ WARNING: This will permanently delete the following ${existingTasksToRemove.length} item(s):` 3354 | ) 3355 | ); 3356 | console.log(); 3357 | 3358 | existingTasksToRemove.forEach(({ id, task }) => { 3359 | if (!task) return; // Should not happen due to taskExists check, but safeguard 3360 | if (task.isSubtask) { 3361 | // Subtask - title is directly on the task object 3362 | console.log( 3363 | chalk.white(` Subtask ${id}: ${task.title || '(no title)'}`) 3364 | ); 3365 | // Optionally show parent context if available 3366 | if (task.parentTask) { 3367 | console.log( 3368 | chalk.gray( 3369 | ` (Parent: ${task.parentTask.id} - ${task.parentTask.title || '(no title)'})` 3370 | ) 3371 | ); 3372 | } 3373 | } else { 3374 | // Main task - title is directly on the task object 3375 | console.log( 3376 | chalk.white.bold(` Task ${id}: ${task.title || '(no title)'}`) 3377 | ); 3378 | } 3379 | }); 3380 | 3381 | if (totalSubtasksToDelete > 0) { 3382 | console.log( 3383 | chalk.yellow( 3384 | `⚠️ This will also delete ${totalSubtasksToDelete} subtasks associated with the selected main tasks!` 3385 | ) 3386 | ); 3387 | } 3388 | 3389 | if (dependentTaskMessages.length > 0) { 3390 | console.log( 3391 | chalk.yellow( 3392 | '⚠️ Warning: Dependencies on the following tasks will be removed:' 3393 | ) 3394 | ); 3395 | dependentTaskMessages.forEach((msg) => 3396 | console.log(chalk.yellow(msg)) 3397 | ); 3398 | } 3399 | 3400 | console.log(); 3401 | 3402 | const { confirm } = await inquirer.prompt([ 3403 | { 3404 | type: 'confirm', 3405 | name: 'confirm', 3406 | message: chalk.red.bold( 3407 | `Are you sure you want to permanently delete these ${existingTasksToRemove.length} item(s)?` 3408 | ), 3409 | default: false 3410 | } 3411 | ]); 3412 | 3413 | if (!confirm) { 3414 | console.log(chalk.blue('Task deletion cancelled.')); 3415 | process.exit(0); 3416 | } 3417 | } 3418 | 3419 | const indicator = startLoadingIndicator( 3420 | `Removing ${existingTasksToRemove.length} task(s)/subtask(s)...` 3421 | ); 3422 | 3423 | // Use the string of existing IDs for the core function 3424 | const existingIdsString = existingTasksToRemove 3425 | .map(({ id }) => id) 3426 | .join(','); 3427 | const result = await removeTask( 3428 | taskMaster.getTasksPath(), 3429 | existingIdsString, 3430 | { 3431 | projectRoot: taskMaster.getProjectRoot(), 3432 | tag 3433 | } 3434 | ); 3435 | 3436 | stopLoadingIndicator(indicator); 3437 | 3438 | if (result.success) { 3439 | console.log( 3440 | boxen( 3441 | chalk.green( 3442 | `Successfully removed ${result.removedTasks.length} task(s)/subtask(s).` 3443 | ) + 3444 | (result.message ? `\n\nDetails:\n${result.message}` : '') + 3445 | (result.error 3446 | ? `\n\nWarnings:\n${chalk.yellow(result.error)}` 3447 | : ''), 3448 | { padding: 1, borderColor: 'green', borderStyle: 'round' } 3449 | ) 3450 | ); 3451 | } else { 3452 | console.error( 3453 | boxen( 3454 | chalk.red( 3455 | `Operation completed with errors. Removed ${result.removedTasks.length} task(s)/subtask(s).` 3456 | ) + 3457 | (result.message ? `\n\nDetails:\n${result.message}` : '') + 3458 | (result.error ? `\n\nErrors:\n${chalk.red(result.error)}` : ''), 3459 | { 3460 | padding: 1, 3461 | borderColor: 'red', 3462 | borderStyle: 'round' 3463 | } 3464 | ) 3465 | ); 3466 | process.exit(1); // Exit with error code if any part failed 3467 | } 3468 | 3469 | // Log any initially non-existent IDs again for clarity 3470 | if (nonExistentIds.length > 0) { 3471 | console.warn( 3472 | chalk.yellow( 3473 | `Note: The following IDs were not found initially and were skipped: ${nonExistentIds.join(', ')}` 3474 | ) 3475 | ); 3476 | 3477 | // Exit with error if any removals failed 3478 | if (result.removedTasks.length === 0) { 3479 | process.exit(1); 3480 | } 3481 | } 3482 | } catch (error) { 3483 | console.error( 3484 | chalk.red(`Error: ${error.message || 'An unknown error occurred'}`) 3485 | ); 3486 | process.exit(1); 3487 | } 3488 | }); 3489 | 3490 | // init command (Directly calls the implementation from init.js) 3491 | programInstance 3492 | .command('init') 3493 | .description('Initialize a new project with Task Master structure') 3494 | .option('-y, --yes', 'Skip prompts and use default values') 3495 | .option('-n, --name <name>', 'Project name') 3496 | .option('-d, --description <description>', 'Project description') 3497 | .option('-v, --version <version>', 'Project version', '0.1.0') // Set default here 3498 | .option('-a, --author <author>', 'Author name') 3499 | .option( 3500 | '-r, --rules <rules...>', 3501 | 'List of rules to add (roo, windsurf, cursor, ...). Accepts comma or space separated values.' 3502 | ) 3503 | .option('--skip-install', 'Skip installing dependencies') 3504 | .option('--dry-run', 'Show what would be done without making changes') 3505 | .option('--aliases', 'Add shell aliases (tm, taskmaster)') 3506 | .option('--no-aliases', 'Skip shell aliases (tm, taskmaster)') 3507 | .option('--git', 'Initialize Git repository') 3508 | .option('--no-git', 'Skip Git repository initialization') 3509 | .option('--git-tasks', 'Store tasks in Git') 3510 | .option('--no-git-tasks', 'No Git storage of tasks') 3511 | .action(async (cmdOptions) => { 3512 | // cmdOptions contains parsed arguments 3513 | // Parse rules: accept space or comma separated, default to all available rules 3514 | let selectedProfiles = RULE_PROFILES; 3515 | let rulesExplicitlyProvided = false; 3516 | 3517 | if (cmdOptions.rules && Array.isArray(cmdOptions.rules)) { 3518 | const userSpecifiedProfiles = cmdOptions.rules 3519 | .flatMap((r) => r.split(',')) 3520 | .map((r) => r.trim()) 3521 | .filter(Boolean); 3522 | // Only override defaults if user specified valid rules 3523 | if (userSpecifiedProfiles.length > 0) { 3524 | selectedProfiles = userSpecifiedProfiles; 3525 | rulesExplicitlyProvided = true; 3526 | } 3527 | } 3528 | 3529 | cmdOptions.rules = selectedProfiles; 3530 | cmdOptions.rulesExplicitlyProvided = rulesExplicitlyProvided; 3531 | 3532 | try { 3533 | // Directly call the initializeProject function, passing the parsed options 3534 | await initializeProject(cmdOptions); 3535 | // initializeProject handles its own flow, including potential process.exit() 3536 | } catch (error) { 3537 | console.error( 3538 | chalk.red(`Error during initialization: ${error.message}`) 3539 | ); 3540 | process.exit(1); 3541 | } 3542 | }); 3543 | 3544 | // models command 3545 | programInstance 3546 | .command('models') 3547 | .description('Manage AI model configurations') 3548 | .option( 3549 | '--set-main <model_id>', 3550 | 'Set the primary model for task generation/updates' 3551 | ) 3552 | .option( 3553 | '--set-research <model_id>', 3554 | 'Set the model for research-backed operations' 3555 | ) 3556 | .option( 3557 | '--set-fallback <model_id>', 3558 | 'Set the model to use if the primary fails' 3559 | ) 3560 | .option('--setup', 'Run interactive setup to configure models') 3561 | .option( 3562 | '--openrouter', 3563 | 'Allow setting a custom OpenRouter model ID (use with --set-*) ' 3564 | ) 3565 | .option( 3566 | '--ollama', 3567 | 'Allow setting a custom Ollama model ID (use with --set-*) ' 3568 | ) 3569 | .option( 3570 | '--bedrock', 3571 | 'Allow setting a custom Bedrock model ID (use with --set-*) ' 3572 | ) 3573 | .option( 3574 | '--claude-code', 3575 | 'Allow setting a Claude Code model ID (use with --set-*)' 3576 | ) 3577 | .option( 3578 | '--azure', 3579 | 'Allow setting a custom Azure OpenAI model ID (use with --set-*) ' 3580 | ) 3581 | .option( 3582 | '--vertex', 3583 | 'Allow setting a custom Vertex AI model ID (use with --set-*) ' 3584 | ) 3585 | .option( 3586 | '--gemini-cli', 3587 | 'Allow setting a Gemini CLI model ID (use with --set-*)' 3588 | ) 3589 | .addHelpText( 3590 | 'after', 3591 | ` 3592 | Examples: 3593 | $ task-master models # View current configuration 3594 | $ task-master models --set-main gpt-4o # Set main model (provider inferred) 3595 | $ task-master models --set-research sonar-pro # Set research model 3596 | $ task-master models --set-fallback claude-3-5-sonnet-20241022 # Set fallback 3597 | $ task-master models --set-main my-custom-model --ollama # Set custom Ollama model for main role 3598 | $ task-master models --set-main anthropic.claude-3-sonnet-20240229-v1:0 --bedrock # Set custom Bedrock model for main role 3599 | $ task-master models --set-main some/other-model --openrouter # Set custom OpenRouter model for main role 3600 | $ task-master models --set-main sonnet --claude-code # Set Claude Code model for main role 3601 | $ task-master models --set-main gpt-4o --azure # Set custom Azure OpenAI model for main role 3602 | $ task-master models --set-main claude-3-5-sonnet@20241022 --vertex # Set custom Vertex AI model for main role 3603 | $ task-master models --set-main gemini-2.5-pro --gemini-cli # Set Gemini CLI model for main role 3604 | $ task-master models --setup # Run interactive setup` 3605 | ) 3606 | .action(async (options) => { 3607 | // Initialize TaskMaster 3608 | const taskMaster = initTaskMaster({ 3609 | tasksPath: options.file || false 3610 | }); 3611 | 3612 | const projectRoot = taskMaster.getProjectRoot(); 3613 | 3614 | // Validate flags: cannot use multiple provider flags simultaneously 3615 | const providerFlags = [ 3616 | options.openrouter, 3617 | options.ollama, 3618 | options.bedrock, 3619 | options.claudeCode, 3620 | options.geminiCli 3621 | ].filter(Boolean).length; 3622 | if (providerFlags > 1) { 3623 | console.error( 3624 | chalk.red( 3625 | 'Error: Cannot use multiple provider flags (--openrouter, --ollama, --bedrock, --claude-code, --gemini-cli) simultaneously.' 3626 | ) 3627 | ); 3628 | process.exit(1); 3629 | } 3630 | 3631 | // Determine the primary action based on flags 3632 | const isSetup = options.setup; 3633 | const isSetOperation = 3634 | options.setMain || options.setResearch || options.setFallback; 3635 | 3636 | // --- Execute Action --- 3637 | 3638 | if (isSetup) { 3639 | // Action 1: Run Interactive Setup 3640 | console.log(chalk.blue('Starting interactive model setup...')); // Added feedback 3641 | try { 3642 | await runInteractiveSetup(taskMaster.getProjectRoot()); 3643 | // runInteractiveSetup logs its own completion/error messages 3644 | } catch (setupError) { 3645 | console.error( 3646 | chalk.red('\\nInteractive setup failed unexpectedly:'), 3647 | setupError.message 3648 | ); 3649 | } 3650 | // --- IMPORTANT: Exit after setup --- 3651 | return; // Stop execution here 3652 | } 3653 | 3654 | if (isSetOperation) { 3655 | // Action 2: Perform Direct Set Operations 3656 | let updateOccurred = false; // Track if any update actually happened 3657 | 3658 | if (options.setMain) { 3659 | const result = await setModel('main', options.setMain, { 3660 | projectRoot, 3661 | providerHint: options.openrouter 3662 | ? 'openrouter' 3663 | : options.ollama 3664 | ? 'ollama' 3665 | : options.bedrock 3666 | ? 'bedrock' 3667 | : options.claudeCode 3668 | ? 'claude-code' 3669 | : options.geminiCli 3670 | ? 'gemini-cli' 3671 | : undefined 3672 | }); 3673 | if (result.success) { 3674 | console.log(chalk.green(`✅ ${result.data.message}`)); 3675 | if (result.data.warning) 3676 | console.log(chalk.yellow(result.data.warning)); 3677 | updateOccurred = true; 3678 | } else { 3679 | console.error( 3680 | chalk.red(`❌ Error setting main model: ${result.error.message}`) 3681 | ); 3682 | } 3683 | } 3684 | if (options.setResearch) { 3685 | const result = await setModel('research', options.setResearch, { 3686 | projectRoot, 3687 | providerHint: options.openrouter 3688 | ? 'openrouter' 3689 | : options.ollama 3690 | ? 'ollama' 3691 | : options.bedrock 3692 | ? 'bedrock' 3693 | : options.claudeCode 3694 | ? 'claude-code' 3695 | : options.geminiCli 3696 | ? 'gemini-cli' 3697 | : undefined 3698 | }); 3699 | if (result.success) { 3700 | console.log(chalk.green(`✅ ${result.data.message}`)); 3701 | if (result.data.warning) 3702 | console.log(chalk.yellow(result.data.warning)); 3703 | updateOccurred = true; 3704 | } else { 3705 | console.error( 3706 | chalk.red( 3707 | `❌ Error setting research model: ${result.error.message}` 3708 | ) 3709 | ); 3710 | } 3711 | } 3712 | if (options.setFallback) { 3713 | const result = await setModel('fallback', options.setFallback, { 3714 | projectRoot, 3715 | providerHint: options.openrouter 3716 | ? 'openrouter' 3717 | : options.ollama 3718 | ? 'ollama' 3719 | : options.bedrock 3720 | ? 'bedrock' 3721 | : options.claudeCode 3722 | ? 'claude-code' 3723 | : options.geminiCli 3724 | ? 'gemini-cli' 3725 | : undefined 3726 | }); 3727 | if (result.success) { 3728 | console.log(chalk.green(`✅ ${result.data.message}`)); 3729 | if (result.data.warning) 3730 | console.log(chalk.yellow(result.data.warning)); 3731 | updateOccurred = true; 3732 | } else { 3733 | console.error( 3734 | chalk.red( 3735 | `❌ Error setting fallback model: ${result.error.message}` 3736 | ) 3737 | ); 3738 | } 3739 | } 3740 | 3741 | // Optional: Add a final confirmation if any update occurred 3742 | if (updateOccurred) { 3743 | console.log(chalk.blue('\nModel configuration updated.')); 3744 | } else { 3745 | console.log( 3746 | chalk.yellow( 3747 | '\nNo model configuration changes were made (or errors occurred).' 3748 | ) 3749 | ); 3750 | } 3751 | 3752 | // --- IMPORTANT: Exit after set operations --- 3753 | return; // Stop execution here 3754 | } 3755 | 3756 | // Action 3: Display Full Status (Only runs if no setup and no set flags) 3757 | console.log(chalk.blue('Fetching current model configuration...')); // Added feedback 3758 | const configResult = await getModelConfiguration({ projectRoot }); 3759 | const availableResult = await getAvailableModelsList({ projectRoot }); 3760 | const apiKeyStatusResult = await getApiKeyStatusReport({ projectRoot }); 3761 | 3762 | // 1. Display Active Models 3763 | if (!configResult.success) { 3764 | console.error( 3765 | chalk.red( 3766 | `❌ Error fetching configuration: ${configResult.error.message}` 3767 | ) 3768 | ); 3769 | } else { 3770 | displayModelConfiguration( 3771 | configResult.data, 3772 | availableResult.data?.models || [] 3773 | ); 3774 | } 3775 | 3776 | // 2. Display API Key Status 3777 | if (apiKeyStatusResult.success) { 3778 | displayApiKeyStatus(apiKeyStatusResult.data.report); 3779 | } else { 3780 | console.error( 3781 | chalk.yellow( 3782 | `⚠️ Warning: Could not display API Key status: ${apiKeyStatusResult.error.message}` 3783 | ) 3784 | ); 3785 | } 3786 | 3787 | // 3. Display Other Available Models (Filtered) 3788 | if (availableResult.success) { 3789 | const activeIds = configResult.success 3790 | ? [ 3791 | configResult.data.activeModels.main.modelId, 3792 | configResult.data.activeModels.research.modelId, 3793 | configResult.data.activeModels.fallback?.modelId 3794 | ].filter(Boolean) 3795 | : []; 3796 | const displayableAvailable = availableResult.data.models.filter( 3797 | (m) => !activeIds.includes(m.modelId) && !m.modelId.startsWith('[') 3798 | ); 3799 | displayAvailableModels(displayableAvailable); 3800 | } else { 3801 | console.error( 3802 | chalk.yellow( 3803 | `⚠️ Warning: Could not display available models: ${availableResult.error.message}` 3804 | ) 3805 | ); 3806 | } 3807 | 3808 | // 4. Conditional Hint if Config File is Missing 3809 | const configExists = isConfigFilePresent(projectRoot); 3810 | if (!configExists) { 3811 | console.log( 3812 | chalk.yellow( 3813 | "\\nHint: Run 'task-master models --setup' to create or update your configuration." 3814 | ) 3815 | ); 3816 | } 3817 | // --- IMPORTANT: Exit after displaying status --- 3818 | return; // Stop execution here 3819 | }); 3820 | 3821 | // response-language command 3822 | programInstance 3823 | .command('lang') 3824 | .description('Manage response language settings') 3825 | .option('--response <response_language>', 'Set the response language') 3826 | .option('--setup', 'Run interactive setup to configure response language') 3827 | .action(async (options) => { 3828 | const taskMaster = initTaskMaster({}); 3829 | const projectRoot = taskMaster.getProjectRoot(); // Find project root for context 3830 | const { response, setup } = options; 3831 | let responseLanguage = response !== undefined ? response : 'English'; 3832 | if (setup) { 3833 | console.log( 3834 | chalk.blue('Starting interactive response language setup...') 3835 | ); 3836 | try { 3837 | const userResponse = await inquirer.prompt([ 3838 | { 3839 | type: 'input', 3840 | name: 'responseLanguage', 3841 | message: 'Input your preferred response language', 3842 | default: 'English' 3843 | } 3844 | ]); 3845 | 3846 | console.log( 3847 | chalk.blue( 3848 | 'Response language set to:', 3849 | userResponse.responseLanguage 3850 | ) 3851 | ); 3852 | responseLanguage = userResponse.responseLanguage; 3853 | } catch (setupError) { 3854 | console.error( 3855 | chalk.red('\\nInteractive setup failed unexpectedly:'), 3856 | setupError.message 3857 | ); 3858 | } 3859 | } 3860 | 3861 | const result = setResponseLanguage(responseLanguage, { 3862 | projectRoot 3863 | }); 3864 | 3865 | if (result.success) { 3866 | console.log(chalk.green(`✅ ${result.data.message}`)); 3867 | } else { 3868 | console.error( 3869 | chalk.red( 3870 | `❌ Error setting response language: ${result.error.message}` 3871 | ) 3872 | ); 3873 | process.exit(1); 3874 | } 3875 | }); 3876 | 3877 | // move-task command 3878 | programInstance 3879 | .command('move') 3880 | .description( 3881 | 'Move tasks between tags or reorder within tags. Supports cross-tag moves with dependency resolution options.' 3882 | ) 3883 | .option( 3884 | '-f, --file <file>', 3885 | 'Path to the tasks file', 3886 | TASKMASTER_TASKS_FILE 3887 | ) 3888 | .option( 3889 | '--from <id>', 3890 | 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")' 3891 | ) 3892 | .option( 3893 | '--to <id>', 3894 | 'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated' 3895 | ) 3896 | .option('--tag <tag>', 'Specify tag context for task operations') 3897 | .option('--from-tag <tag>', 'Source tag for cross-tag moves') 3898 | .option('--to-tag <tag>', 'Target tag for cross-tag moves') 3899 | .option('--with-dependencies', 'Move dependent tasks along with main task') 3900 | .option('--ignore-dependencies', 'Break cross-tag dependencies during move') 3901 | .action(async (options) => { 3902 | // Helper function to show move command help - defined in scope for proper encapsulation 3903 | function showMoveHelp() { 3904 | console.log( 3905 | chalk.white.bold('Move Command Help') + 3906 | '\n\n' + 3907 | chalk.cyan('Move tasks between tags or reorder within tags.') + 3908 | '\n\n' + 3909 | chalk.yellow.bold('Within-Tag Moves:') + 3910 | '\n' + 3911 | chalk.white(' task-master move --from=5 --to=7') + 3912 | '\n' + 3913 | chalk.white(' task-master move --from=5.2 --to=7.3') + 3914 | '\n' + 3915 | chalk.white(' task-master move --from=5,6,7 --to=10,11,12') + 3916 | '\n\n' + 3917 | chalk.yellow.bold('Cross-Tag Moves:') + 3918 | '\n' + 3919 | chalk.white( 3920 | ' task-master move --from=5 --from-tag=backlog --to-tag=in-progress' 3921 | ) + 3922 | '\n' + 3923 | chalk.white( 3924 | ' task-master move --from=5,6 --from-tag=backlog --to-tag=done' 3925 | ) + 3926 | '\n\n' + 3927 | chalk.yellow.bold('Dependency Resolution:') + 3928 | '\n' + 3929 | chalk.white(' # Move with dependencies') + 3930 | '\n' + 3931 | chalk.white( 3932 | ' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies' 3933 | ) + 3934 | '\n\n' + 3935 | chalk.white(' # Break dependencies') + 3936 | '\n' + 3937 | chalk.white( 3938 | ' task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies' 3939 | ) + 3940 | '\n\n' + 3941 | '\n' + 3942 | chalk.yellow.bold('Best Practices:') + 3943 | '\n' + 3944 | chalk.white( 3945 | ' • Use --with-dependencies to move dependent tasks together' 3946 | ) + 3947 | '\n' + 3948 | chalk.white( 3949 | ' • Use --ignore-dependencies to break cross-tag dependencies' 3950 | ) + 3951 | '\n' + 3952 | chalk.white( 3953 | ' • Check dependencies first: task-master validate-dependencies' 3954 | ) + 3955 | '\n' + 3956 | chalk.white( 3957 | ' • Fix dependency issues: task-master fix-dependencies' 3958 | ) + 3959 | '\n\n' + 3960 | chalk.yellow.bold('Error Resolution:') + 3961 | '\n' + 3962 | chalk.white( 3963 | ' • Cross-tag dependency conflicts: Use --with-dependencies or --ignore-dependencies' 3964 | ) + 3965 | '\n' + 3966 | chalk.white( 3967 | ' • Subtask movement: Promote subtask first with remove-subtask --convert' 3968 | ) + 3969 | '\n' + 3970 | chalk.white( 3971 | ' • Invalid tags: Check available tags with task-master tags' 3972 | ) + 3973 | '\n\n' + 3974 | chalk.gray('For more help, run: task-master move --help') 3975 | ); 3976 | } 3977 | 3978 | // Helper function to handle cross-tag move logic 3979 | async function handleCrossTagMove(moveContext, options) { 3980 | const { sourceId, sourceTag, toTag, taskMaster } = moveContext; 3981 | 3982 | if (!sourceId) { 3983 | console.error( 3984 | chalk.red('Error: --from parameter is required for cross-tag moves') 3985 | ); 3986 | showMoveHelp(); 3987 | process.exit(1); 3988 | } 3989 | 3990 | const sourceIds = sourceId.split(',').map((id) => id.trim()); 3991 | const moveOptions = { 3992 | withDependencies: options.withDependencies || false, 3993 | ignoreDependencies: options.ignoreDependencies || false 3994 | }; 3995 | 3996 | console.log( 3997 | chalk.blue( 3998 | `Moving tasks ${sourceIds.join(', ')} from "${sourceTag}" to "${toTag}"...` 3999 | ) 4000 | ); 4001 | 4002 | const result = await moveTasksBetweenTags( 4003 | taskMaster.getTasksPath(), 4004 | sourceIds, 4005 | sourceTag, 4006 | toTag, 4007 | moveOptions, 4008 | { projectRoot: taskMaster.getProjectRoot() } 4009 | ); 4010 | 4011 | console.log(chalk.green(`✓ ${result.message}`)); 4012 | 4013 | // Print any tips returned from the move operation (e.g., after ignoring dependencies) 4014 | if (Array.isArray(result.tips) && result.tips.length > 0) { 4015 | console.log('\n' + chalk.yellow.bold('Next Steps:')); 4016 | result.tips.forEach((t) => console.log(chalk.white(` • ${t}`))); 4017 | } 4018 | 4019 | // Check if source tag still contains tasks before regenerating files 4020 | const tasksData = readJSON( 4021 | taskMaster.getTasksPath(), 4022 | taskMaster.getProjectRoot(), 4023 | sourceTag 4024 | ); 4025 | const sourceTagHasTasks = 4026 | tasksData && 4027 | Array.isArray(tasksData.tasks) && 4028 | tasksData.tasks.length > 0; 4029 | 4030 | // Generate task files for the affected tags 4031 | await generateTaskFiles( 4032 | taskMaster.getTasksPath(), 4033 | path.dirname(taskMaster.getTasksPath()), 4034 | { tag: toTag, projectRoot: taskMaster.getProjectRoot() } 4035 | ); 4036 | 4037 | // Only regenerate source tag files if it still contains tasks 4038 | if (sourceTagHasTasks) { 4039 | await generateTaskFiles( 4040 | taskMaster.getTasksPath(), 4041 | path.dirname(taskMaster.getTasksPath()), 4042 | { tag: sourceTag, projectRoot: taskMaster.getProjectRoot() } 4043 | ); 4044 | } 4045 | } 4046 | 4047 | // Helper function to handle within-tag move logic 4048 | async function handleWithinTagMove(moveContext) { 4049 | const { sourceId, destinationId, tag, taskMaster } = moveContext; 4050 | 4051 | if (!sourceId || !destinationId) { 4052 | console.error( 4053 | chalk.red( 4054 | 'Error: Both --from and --to parameters are required for within-tag moves' 4055 | ) 4056 | ); 4057 | console.log( 4058 | chalk.yellow( 4059 | 'Usage: task-master move --from=<sourceId> --to=<destinationId>' 4060 | ) 4061 | ); 4062 | process.exit(1); 4063 | } 4064 | 4065 | // Check if we're moving multiple tasks (comma-separated IDs) 4066 | const sourceIds = sourceId.split(',').map((id) => id.trim()); 4067 | const destinationIds = destinationId.split(',').map((id) => id.trim()); 4068 | 4069 | // Validate that the number of source and destination IDs match 4070 | if (sourceIds.length !== destinationIds.length) { 4071 | console.error( 4072 | chalk.red( 4073 | 'Error: The number of source and destination IDs must match' 4074 | ) 4075 | ); 4076 | console.log( 4077 | chalk.yellow('Example: task-master move --from=5,6,7 --to=10,11,12') 4078 | ); 4079 | process.exit(1); 4080 | } 4081 | 4082 | // If moving multiple tasks 4083 | if (sourceIds.length > 1) { 4084 | console.log( 4085 | chalk.blue( 4086 | `Moving multiple tasks: ${sourceIds.join(', ')} to ${destinationIds.join(', ')}...` 4087 | ) 4088 | ); 4089 | 4090 | // Read tasks data once to validate destination IDs 4091 | const tasksData = readJSON( 4092 | taskMaster.getTasksPath(), 4093 | taskMaster.getProjectRoot(), 4094 | tag 4095 | ); 4096 | if (!tasksData || !tasksData.tasks) { 4097 | console.error( 4098 | chalk.red( 4099 | `Error: Invalid or missing tasks file at ${taskMaster.getTasksPath()}` 4100 | ) 4101 | ); 4102 | process.exit(1); 4103 | } 4104 | 4105 | // Collect errors during move attempts 4106 | const moveErrors = []; 4107 | const successfulMoves = []; 4108 | 4109 | // Move tasks one by one 4110 | for (let i = 0; i < sourceIds.length; i++) { 4111 | const fromId = sourceIds[i]; 4112 | const toId = destinationIds[i]; 4113 | 4114 | // Skip if source and destination are the same 4115 | if (fromId === toId) { 4116 | console.log( 4117 | chalk.yellow(`Skipping ${fromId} -> ${toId} (same ID)`) 4118 | ); 4119 | continue; 4120 | } 4121 | 4122 | console.log( 4123 | chalk.blue(`Moving task/subtask ${fromId} to ${toId}...`) 4124 | ); 4125 | try { 4126 | await moveTask( 4127 | taskMaster.getTasksPath(), 4128 | fromId, 4129 | toId, 4130 | i === sourceIds.length - 1, 4131 | { projectRoot: taskMaster.getProjectRoot(), tag } 4132 | ); 4133 | console.log( 4134 | chalk.green( 4135 | `✓ Successfully moved task/subtask ${fromId} to ${toId}` 4136 | ) 4137 | ); 4138 | successfulMoves.push({ fromId, toId }); 4139 | } catch (error) { 4140 | const errorInfo = { 4141 | fromId, 4142 | toId, 4143 | error: error.message 4144 | }; 4145 | moveErrors.push(errorInfo); 4146 | console.error( 4147 | chalk.red(`Error moving ${fromId} to ${toId}: ${error.message}`) 4148 | ); 4149 | // Continue with the next task rather than exiting 4150 | } 4151 | } 4152 | 4153 | // Display summary after all moves are attempted 4154 | if (moveErrors.length > 0) { 4155 | console.log(chalk.yellow('\n--- Move Operation Summary ---')); 4156 | console.log( 4157 | chalk.green( 4158 | `✓ Successfully moved: ${successfulMoves.length} tasks` 4159 | ) 4160 | ); 4161 | console.log( 4162 | chalk.red(`✗ Failed to move: ${moveErrors.length} tasks`) 4163 | ); 4164 | 4165 | if (successfulMoves.length > 0) { 4166 | console.log(chalk.cyan('\nSuccessful moves:')); 4167 | successfulMoves.forEach(({ fromId, toId }) => { 4168 | console.log(chalk.cyan(` ${fromId} → ${toId}`)); 4169 | }); 4170 | } 4171 | 4172 | console.log(chalk.red('\nFailed moves:')); 4173 | moveErrors.forEach(({ fromId, toId, error }) => { 4174 | console.log(chalk.red(` ${fromId} → ${toId}: ${error}`)); 4175 | }); 4176 | 4177 | console.log( 4178 | chalk.yellow( 4179 | '\nNote: Some tasks were moved successfully. Check the errors above for failed moves.' 4180 | ) 4181 | ); 4182 | } else { 4183 | console.log(chalk.green('\n✓ All tasks moved successfully!')); 4184 | } 4185 | } else { 4186 | // Moving a single task (existing logic) 4187 | console.log( 4188 | chalk.blue(`Moving task/subtask ${sourceId} to ${destinationId}...`) 4189 | ); 4190 | 4191 | const result = await moveTask( 4192 | taskMaster.getTasksPath(), 4193 | sourceId, 4194 | destinationId, 4195 | true, 4196 | { projectRoot: taskMaster.getProjectRoot(), tag } 4197 | ); 4198 | console.log( 4199 | chalk.green( 4200 | `✓ Successfully moved task/subtask ${sourceId} to ${destinationId}` 4201 | ) 4202 | ); 4203 | } 4204 | } 4205 | 4206 | // Helper function to handle move errors 4207 | function handleMoveError(error, moveContext) { 4208 | console.error(chalk.red(`Error: ${error.message}`)); 4209 | 4210 | // Enhanced error handling with structured error objects 4211 | if (error.code === 'CROSS_TAG_DEPENDENCY_CONFLICTS') { 4212 | // Use structured error data 4213 | const conflicts = error.data.conflicts || []; 4214 | const taskIds = error.data.taskIds || []; 4215 | displayCrossTagDependencyError( 4216 | conflicts, 4217 | moveContext.sourceTag, 4218 | moveContext.toTag, 4219 | taskIds.join(', ') 4220 | ); 4221 | } else if (error.code === 'CANNOT_MOVE_SUBTASK') { 4222 | // Use structured error data 4223 | const taskId = 4224 | error.data.taskId || moveContext.sourceId?.split(',')[0]; 4225 | displaySubtaskMoveError( 4226 | taskId, 4227 | moveContext.sourceTag, 4228 | moveContext.toTag 4229 | ); 4230 | } else if ( 4231 | error.code === 'SOURCE_TARGET_TAGS_SAME' || 4232 | error.code === 'SAME_SOURCE_TARGET_TAG' 4233 | ) { 4234 | displayInvalidTagCombinationError( 4235 | moveContext.sourceTag, 4236 | moveContext.toTag, 4237 | 'Source and target tags are identical' 4238 | ); 4239 | } else { 4240 | // General error - show dependency validation hints 4241 | displayDependencyValidationHints('after-error'); 4242 | } 4243 | 4244 | process.exit(1); 4245 | } 4246 | 4247 | // Initialize TaskMaster 4248 | const taskMaster = initTaskMaster({ 4249 | tasksPath: options.file || true, 4250 | tag: options.tag 4251 | }); 4252 | 4253 | const sourceId = options.from; 4254 | const destinationId = options.to; 4255 | const fromTag = options.fromTag; 4256 | const toTag = options.toTag; 4257 | 4258 | const tag = taskMaster.getCurrentTag(); 4259 | 4260 | // Get the source tag - fallback to current tag if not provided 4261 | const sourceTag = fromTag || taskMaster.getCurrentTag(); 4262 | 4263 | // Check if this is a cross-tag move (different tags) 4264 | const isCrossTagMove = sourceTag && toTag && sourceTag !== toTag; 4265 | 4266 | // Initialize move context with all relevant data 4267 | const moveContext = { 4268 | sourceId, 4269 | destinationId, 4270 | sourceTag, 4271 | toTag, 4272 | tag, 4273 | taskMaster 4274 | }; 4275 | 4276 | try { 4277 | if (isCrossTagMove) { 4278 | // Cross-tag move logic 4279 | await handleCrossTagMove(moveContext, options); 4280 | } else { 4281 | // Within-tag move logic 4282 | await handleWithinTagMove(moveContext); 4283 | } 4284 | } catch (error) { 4285 | const errMsg = String(error && (error.message || error)); 4286 | if (errMsg.includes('already exists in target tag')) { 4287 | console.error(chalk.red(`Error: ${errMsg}`)); 4288 | console.log( 4289 | '\n' + 4290 | chalk.yellow.bold('Conflict: ID already exists in target tag') + 4291 | '\n' + 4292 | chalk.white( 4293 | ' • Choose a different target tag without conflicting IDs' 4294 | ) + 4295 | '\n' + 4296 | chalk.white( 4297 | ' • Move a different set of IDs (avoid existing ones)' 4298 | ) + 4299 | '\n' + 4300 | chalk.white( 4301 | ' • If needed, move within-tag to a new ID first, then cross-tag move' 4302 | ) 4303 | ); 4304 | process.exit(1); 4305 | } 4306 | handleMoveError(error, moveContext); 4307 | } 4308 | }); 4309 | 4310 | // Add/remove profile rules command 4311 | programInstance 4312 | .command('rules [action] [profiles...]') 4313 | .description( 4314 | `Add or remove rules for one or more profiles. Valid actions: ${Object.values(RULES_ACTIONS).join(', ')} (e.g., task-master rules ${RULES_ACTIONS.ADD} windsurf roo)` 4315 | ) 4316 | .option( 4317 | '-f, --force', 4318 | 'Skip confirmation prompt when removing rules (dangerous)' 4319 | ) 4320 | .option( 4321 | `--${RULES_SETUP_ACTION}`, 4322 | 'Run interactive setup to select rule profiles to add' 4323 | ) 4324 | .addHelpText( 4325 | 'after', 4326 | ` 4327 | Examples: 4328 | $ task-master rules ${RULES_ACTIONS.ADD} windsurf roo # Add Windsurf and Roo rule sets 4329 | $ task-master rules ${RULES_ACTIONS.REMOVE} windsurf # Remove Windsurf rule set 4330 | $ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles` 4331 | ) 4332 | .action(async (action, profiles, options) => { 4333 | const taskMaster = initTaskMaster({}); 4334 | const projectRoot = taskMaster.getProjectRoot(); 4335 | if (!projectRoot) { 4336 | console.error(chalk.red('Error: Could not find project root.')); 4337 | process.exit(1); 4338 | } 4339 | 4340 | /** 4341 | * 'task-master rules --setup' action: 4342 | * 4343 | * Launches an interactive prompt to select which rule profiles to add to the current project. 4344 | * This does NOT perform project initialization or ask about shell aliases—only rules selection. 4345 | * 4346 | * Example usage: 4347 | * $ task-master rules --setup 4348 | * 4349 | * Useful for adding rules after project creation. 4350 | * 4351 | * The list of profiles is always up-to-date with the available profiles. 4352 | */ 4353 | if (options[RULES_SETUP_ACTION]) { 4354 | // Run interactive rules setup ONLY (no project init) 4355 | const selectedRuleProfiles = await runInteractiveProfilesSetup(); 4356 | 4357 | if (!selectedRuleProfiles || selectedRuleProfiles.length === 0) { 4358 | console.log(chalk.yellow('No profiles selected. Exiting.')); 4359 | return; 4360 | } 4361 | 4362 | console.log( 4363 | chalk.blue( 4364 | `Installing ${selectedRuleProfiles.length} selected profile(s)...` 4365 | ) 4366 | ); 4367 | 4368 | for (let i = 0; i < selectedRuleProfiles.length; i++) { 4369 | const profile = selectedRuleProfiles[i]; 4370 | console.log( 4371 | chalk.blue( 4372 | `Processing profile ${i + 1}/${selectedRuleProfiles.length}: ${profile}...` 4373 | ) 4374 | ); 4375 | 4376 | if (!isValidProfile(profile)) { 4377 | console.warn( 4378 | `Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.` 4379 | ); 4380 | continue; 4381 | } 4382 | const profileConfig = getRulesProfile(profile); 4383 | 4384 | const addResult = convertAllRulesToProfileRules( 4385 | projectRoot, 4386 | profileConfig 4387 | ); 4388 | 4389 | console.log(chalk.green(generateProfileSummary(profile, addResult))); 4390 | } 4391 | 4392 | console.log( 4393 | chalk.green( 4394 | `\nCompleted installation of all ${selectedRuleProfiles.length} profile(s).` 4395 | ) 4396 | ); 4397 | return; 4398 | } 4399 | 4400 | // Validate action for non-setup mode 4401 | if (!action || !isValidRulesAction(action)) { 4402 | console.error( 4403 | chalk.red( 4404 | `Error: Invalid or missing action '${action || 'none'}'. Valid actions are: ${Object.values(RULES_ACTIONS).join(', ')}` 4405 | ) 4406 | ); 4407 | console.error( 4408 | chalk.yellow( 4409 | `For interactive setup, use: task-master rules --${RULES_SETUP_ACTION}` 4410 | ) 4411 | ); 4412 | process.exit(1); 4413 | } 4414 | 4415 | if (!profiles || profiles.length === 0) { 4416 | console.error( 4417 | 'Please specify at least one rule profile (e.g., windsurf, roo).' 4418 | ); 4419 | process.exit(1); 4420 | } 4421 | 4422 | // Support both space- and comma-separated profile lists 4423 | const expandedProfiles = profiles 4424 | .flatMap((b) => b.split(',').map((s) => s.trim())) 4425 | .filter(Boolean); 4426 | 4427 | if (action === RULES_ACTIONS.REMOVE) { 4428 | let confirmed = true; 4429 | if (!options.force) { 4430 | // Check if this removal would leave no profiles remaining 4431 | if (wouldRemovalLeaveNoProfiles(projectRoot, expandedProfiles)) { 4432 | const installedProfiles = getInstalledProfiles(projectRoot); 4433 | confirmed = await confirmRemoveAllRemainingProfiles( 4434 | expandedProfiles, 4435 | installedProfiles 4436 | ); 4437 | } else { 4438 | confirmed = await confirmProfilesRemove(expandedProfiles); 4439 | } 4440 | } 4441 | if (!confirmed) { 4442 | console.log(chalk.yellow('Aborted: No rules were removed.')); 4443 | return; 4444 | } 4445 | } 4446 | 4447 | const removalResults = []; 4448 | const addResults = []; 4449 | 4450 | for (const profile of expandedProfiles) { 4451 | if (!isValidProfile(profile)) { 4452 | console.warn( 4453 | `Rule profile for "${profile}" not found. Valid profiles: ${RULE_PROFILES.join(', ')}. Skipping.` 4454 | ); 4455 | continue; 4456 | } 4457 | const profileConfig = getRulesProfile(profile); 4458 | 4459 | if (action === RULES_ACTIONS.ADD) { 4460 | console.log(chalk.blue(`Adding rules for profile: ${profile}...`)); 4461 | const addResult = convertAllRulesToProfileRules( 4462 | projectRoot, 4463 | profileConfig 4464 | ); 4465 | console.log( 4466 | chalk.blue(`Completed adding rules for profile: ${profile}`) 4467 | ); 4468 | 4469 | // Store result with profile name for summary 4470 | addResults.push({ 4471 | profileName: profile, 4472 | success: addResult.success, 4473 | failed: addResult.failed 4474 | }); 4475 | 4476 | console.log(chalk.green(generateProfileSummary(profile, addResult))); 4477 | } else if (action === RULES_ACTIONS.REMOVE) { 4478 | console.log(chalk.blue(`Removing rules for profile: ${profile}...`)); 4479 | const result = removeProfileRules(projectRoot, profileConfig); 4480 | removalResults.push(result); 4481 | console.log( 4482 | chalk.green(generateProfileRemovalSummary(profile, result)) 4483 | ); 4484 | } else { 4485 | console.error( 4486 | `Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".` 4487 | ); 4488 | process.exit(1); 4489 | } 4490 | } 4491 | 4492 | // Print summary for additions 4493 | if (action === RULES_ACTIONS.ADD && addResults.length > 0) { 4494 | const { allSuccessfulProfiles, totalSuccess, totalFailed } = 4495 | categorizeProfileResults(addResults); 4496 | 4497 | if (allSuccessfulProfiles.length > 0) { 4498 | console.log( 4499 | chalk.green( 4500 | `\nSuccessfully processed profiles: ${allSuccessfulProfiles.join(', ')}` 4501 | ) 4502 | ); 4503 | 4504 | // Create a descriptive summary 4505 | if (totalSuccess > 0) { 4506 | console.log( 4507 | chalk.green( 4508 | `Total: ${totalSuccess} files processed, ${totalFailed} failed.` 4509 | ) 4510 | ); 4511 | } else { 4512 | console.log( 4513 | chalk.green( 4514 | `Total: ${allSuccessfulProfiles.length} profile(s) set up successfully.` 4515 | ) 4516 | ); 4517 | } 4518 | } 4519 | } 4520 | 4521 | // Print summary for removals 4522 | if (action === RULES_ACTIONS.REMOVE && removalResults.length > 0) { 4523 | const { 4524 | successfulRemovals, 4525 | skippedRemovals, 4526 | failedRemovals, 4527 | removalsWithNotices 4528 | } = categorizeRemovalResults(removalResults); 4529 | 4530 | if (successfulRemovals.length > 0) { 4531 | console.log( 4532 | chalk.green( 4533 | `\nSuccessfully removed profiles for: ${successfulRemovals.join(', ')}` 4534 | ) 4535 | ); 4536 | } 4537 | if (skippedRemovals.length > 0) { 4538 | console.log( 4539 | chalk.yellow( 4540 | `Skipped (default or protected): ${skippedRemovals.join(', ')}` 4541 | ) 4542 | ); 4543 | } 4544 | if (failedRemovals.length > 0) { 4545 | console.log(chalk.red('\nErrors occurred:')); 4546 | failedRemovals.forEach((r) => { 4547 | console.log(chalk.red(` ${r.profileName}: ${r.error}`)); 4548 | }); 4549 | } 4550 | // Display notices about preserved files/configurations 4551 | if (removalsWithNotices.length > 0) { 4552 | console.log(chalk.cyan('\nNotices:')); 4553 | removalsWithNotices.forEach((r) => { 4554 | console.log(chalk.cyan(` ${r.profileName}: ${r.notice}`)); 4555 | }); 4556 | } 4557 | 4558 | // Overall summary 4559 | const totalProcessed = removalResults.length; 4560 | const totalSuccessful = successfulRemovals.length; 4561 | const totalSkipped = skippedRemovals.length; 4562 | const totalFailed = failedRemovals.length; 4563 | 4564 | console.log( 4565 | chalk.blue( 4566 | `\nTotal: ${totalProcessed} profile(s) processed - ${totalSuccessful} removed, ${totalSkipped} skipped, ${totalFailed} failed.` 4567 | ) 4568 | ); 4569 | } 4570 | }); 4571 | 4572 | programInstance 4573 | .command('migrate') 4574 | .description( 4575 | 'Migrate existing project to use the new .taskmaster directory structure' 4576 | ) 4577 | .option( 4578 | '-f, --force', 4579 | 'Force migration even if .taskmaster directory already exists' 4580 | ) 4581 | .option( 4582 | '--backup', 4583 | 'Create backup of old files before migration (default: false)', 4584 | false 4585 | ) 4586 | .option( 4587 | '--cleanup', 4588 | 'Remove old files after successful migration (default: true)', 4589 | true 4590 | ) 4591 | .option('-y, --yes', 'Skip confirmation prompts') 4592 | .option( 4593 | '--dry-run', 4594 | 'Show what would be migrated without actually moving files' 4595 | ) 4596 | .action(async (options) => { 4597 | try { 4598 | await migrateProject(options); 4599 | } catch (error) { 4600 | console.error(chalk.red('Error during migration:'), error.message); 4601 | process.exit(1); 4602 | } 4603 | }); 4604 | 4605 | // sync-readme command 4606 | programInstance 4607 | .command('sync-readme') 4608 | .description('Sync the current task list to README.md in the project root') 4609 | .option( 4610 | '-f, --file <file>', 4611 | 'Path to the tasks file', 4612 | TASKMASTER_TASKS_FILE 4613 | ) 4614 | .option('--with-subtasks', 'Include subtasks in the README output') 4615 | .option( 4616 | '-s, --status <status>', 4617 | 'Show only tasks matching this status (e.g., pending, done)' 4618 | ) 4619 | .option('-t, --tag <tag>', 'Tag to use for the task list (default: master)') 4620 | .action(async (options) => { 4621 | // Initialize TaskMaster 4622 | const taskMaster = initTaskMaster({ 4623 | tasksPath: options.file || true, 4624 | tag: options.tag 4625 | }); 4626 | 4627 | const withSubtasks = options.withSubtasks || false; 4628 | const status = options.status || null; 4629 | 4630 | const tag = taskMaster.getCurrentTag(); 4631 | 4632 | console.log( 4633 | chalk.blue( 4634 | `📝 Syncing tasks to README.md${withSubtasks ? ' (with subtasks)' : ''}${status ? ` (status: ${status})` : ''}...` 4635 | ) 4636 | ); 4637 | 4638 | const success = await syncTasksToReadme(taskMaster.getProjectRoot(), { 4639 | withSubtasks, 4640 | status, 4641 | tasksPath: taskMaster.getTasksPath(), 4642 | tag 4643 | }); 4644 | 4645 | if (!success) { 4646 | console.error(chalk.red('❌ Failed to sync tasks to README.md')); 4647 | process.exit(1); 4648 | } 4649 | }); 4650 | 4651 | // ===== TAG MANAGEMENT COMMANDS ===== 4652 | 4653 | // add-tag command 4654 | programInstance 4655 | .command('add-tag') 4656 | .description('Create a new tag context for organizing tasks') 4657 | .argument( 4658 | '[tagName]', 4659 | 'Name of the new tag to create (optional when using --from-branch)' 4660 | ) 4661 | .option( 4662 | '-f, --file <file>', 4663 | 'Path to the tasks file', 4664 | TASKMASTER_TASKS_FILE 4665 | ) 4666 | .option( 4667 | '--copy-from-current', 4668 | 'Copy tasks from the current tag to the new tag' 4669 | ) 4670 | .option( 4671 | '--copy-from <tag>', 4672 | 'Copy tasks from the specified tag to the new tag' 4673 | ) 4674 | .option( 4675 | '--from-branch', 4676 | 'Create tag name from current git branch (ignores tagName argument)' 4677 | ) 4678 | .option('-d, --description <text>', 'Optional description for the tag') 4679 | .action(async (tagName, options) => { 4680 | try { 4681 | // Initialize TaskMaster 4682 | const taskMaster = initTaskMaster({ 4683 | tasksPath: options.file || true 4684 | }); 4685 | const tasksPath = taskMaster.getTasksPath(); 4686 | 4687 | // Validate tasks file exists 4688 | if (!fs.existsSync(tasksPath)) { 4689 | console.error( 4690 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 4691 | ); 4692 | console.log( 4693 | chalk.yellow( 4694 | 'Hint: Run task-master init or task-master parse-prd to create tasks.json first' 4695 | ) 4696 | ); 4697 | process.exit(1); 4698 | } 4699 | 4700 | // Validate that either tagName is provided or --from-branch is used 4701 | if (!tagName && !options.fromBranch) { 4702 | console.error( 4703 | chalk.red( 4704 | 'Error: Either tagName argument or --from-branch option is required.' 4705 | ) 4706 | ); 4707 | console.log(chalk.yellow('Usage examples:')); 4708 | console.log(chalk.cyan(' task-master add-tag my-tag')); 4709 | console.log(chalk.cyan(' task-master add-tag --from-branch')); 4710 | process.exit(1); 4711 | } 4712 | 4713 | const context = { 4714 | projectRoot: taskMaster.getProjectRoot(), 4715 | commandName: 'add-tag', 4716 | outputType: 'cli' 4717 | }; 4718 | 4719 | // Handle --from-branch option 4720 | if (options.fromBranch) { 4721 | const { createTagFromBranch } = await import( 4722 | './task-manager/tag-management.js' 4723 | ); 4724 | const gitUtils = await import('./utils/git-utils.js'); 4725 | 4726 | // Check if we're in a git repository 4727 | if (!(await gitUtils.isGitRepository(context.projectRoot))) { 4728 | console.error( 4729 | chalk.red( 4730 | 'Error: Not in a git repository. Cannot use --from-branch option.' 4731 | ) 4732 | ); 4733 | process.exit(1); 4734 | } 4735 | 4736 | // Get current git branch 4737 | const currentBranch = await gitUtils.getCurrentBranch( 4738 | context.projectRoot 4739 | ); 4740 | if (!currentBranch) { 4741 | console.error( 4742 | chalk.red('Error: Could not determine current git branch.') 4743 | ); 4744 | process.exit(1); 4745 | } 4746 | 4747 | // Create tag from branch 4748 | const branchOptions = { 4749 | copyFromCurrent: options.copyFromCurrent || false, 4750 | copyFromTag: options.copyFrom, 4751 | description: 4752 | options.description || 4753 | `Tag created from git branch "${currentBranch}"` 4754 | }; 4755 | 4756 | await createTagFromBranch( 4757 | taskMaster.getTasksPath(), 4758 | currentBranch, 4759 | branchOptions, 4760 | context, 4761 | 'text' 4762 | ); 4763 | } else { 4764 | // Regular tag creation 4765 | const createOptions = { 4766 | copyFromCurrent: options.copyFromCurrent || false, 4767 | copyFromTag: options.copyFrom, 4768 | description: options.description 4769 | }; 4770 | 4771 | await createTag( 4772 | taskMaster.getTasksPath(), 4773 | tagName, 4774 | createOptions, 4775 | context, 4776 | 'text' 4777 | ); 4778 | } 4779 | 4780 | // Handle auto-switch if requested 4781 | if (options.autoSwitch) { 4782 | const { useTag } = await import('./task-manager/tag-management.js'); 4783 | const finalTagName = options.fromBranch 4784 | ? (await import('./utils/git-utils.js')).sanitizeBranchNameForTag( 4785 | await (await import('./utils/git-utils.js')).getCurrentBranch( 4786 | projectRoot 4787 | ) 4788 | ) 4789 | : tagName; 4790 | await useTag( 4791 | taskMaster.getTasksPath(), 4792 | finalTagName, 4793 | {}, 4794 | context, 4795 | 'text' 4796 | ); 4797 | } 4798 | } catch (error) { 4799 | console.error(chalk.red(`Error creating tag: ${error.message}`)); 4800 | showAddTagHelp(); 4801 | process.exit(1); 4802 | } 4803 | }) 4804 | .on('error', function (err) { 4805 | console.error(chalk.red(`Error: ${err.message}`)); 4806 | showAddTagHelp(); 4807 | process.exit(1); 4808 | }); 4809 | 4810 | // delete-tag command 4811 | programInstance 4812 | .command('delete-tag') 4813 | .description('Delete an existing tag and all its tasks') 4814 | .argument('<tagName>', 'Name of the tag to delete') 4815 | .option( 4816 | '-f, --file <file>', 4817 | 'Path to the tasks file', 4818 | TASKMASTER_TASKS_FILE 4819 | ) 4820 | .option('-y, --yes', 'Skip confirmation prompts') 4821 | .action(async (tagName, options) => { 4822 | try { 4823 | // Initialize TaskMaster 4824 | const taskMaster = initTaskMaster({ 4825 | tasksPath: options.file || true 4826 | }); 4827 | const tasksPath = taskMaster.getTasksPath(); 4828 | 4829 | // Validate tasks file exists 4830 | if (!fs.existsSync(tasksPath)) { 4831 | console.error( 4832 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 4833 | ); 4834 | process.exit(1); 4835 | } 4836 | 4837 | const deleteOptions = { 4838 | yes: options.yes || false 4839 | }; 4840 | 4841 | const context = { 4842 | projectRoot: taskMaster.getProjectRoot(), 4843 | commandName: 'delete-tag', 4844 | outputType: 'cli' 4845 | }; 4846 | 4847 | await deleteTag( 4848 | taskMaster.getTasksPath(), 4849 | tagName, 4850 | deleteOptions, 4851 | context, 4852 | 'text' 4853 | ); 4854 | } catch (error) { 4855 | console.error(chalk.red(`Error deleting tag: ${error.message}`)); 4856 | showDeleteTagHelp(); 4857 | process.exit(1); 4858 | } 4859 | }) 4860 | .on('error', function (err) { 4861 | console.error(chalk.red(`Error: ${err.message}`)); 4862 | showDeleteTagHelp(); 4863 | process.exit(1); 4864 | }); 4865 | 4866 | // tags command 4867 | programInstance 4868 | .command('tags') 4869 | .description('List all available tags with metadata') 4870 | .option( 4871 | '-f, --file <file>', 4872 | 'Path to the tasks file', 4873 | TASKMASTER_TASKS_FILE 4874 | ) 4875 | .option('--show-metadata', 'Show detailed metadata for each tag') 4876 | .option('--tag <tag>', 'Specify tag context for task operations') 4877 | .action(async (options) => { 4878 | try { 4879 | // Initialize TaskMaster 4880 | const taskMaster = initTaskMaster({ 4881 | tasksPath: options.file || true, 4882 | tag: options.tag 4883 | }); 4884 | const tasksPath = taskMaster.getTasksPath(); 4885 | 4886 | // Validate tasks file exists 4887 | if (!fs.existsSync(tasksPath)) { 4888 | console.error( 4889 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 4890 | ); 4891 | process.exit(1); 4892 | } 4893 | 4894 | const listOptions = { 4895 | showTaskCounts: true, 4896 | showMetadata: options.showMetadata || false 4897 | }; 4898 | 4899 | const context = { 4900 | projectRoot: taskMaster.getProjectRoot(), 4901 | commandName: 'tags', 4902 | outputType: 'cli' 4903 | }; 4904 | 4905 | await tags(taskMaster.getTasksPath(), listOptions, context, 'text'); 4906 | } catch (error) { 4907 | console.error(chalk.red(`Error listing tags: ${error.message}`)); 4908 | showTagsHelp(); 4909 | process.exit(1); 4910 | } 4911 | }) 4912 | .on('error', function (err) { 4913 | console.error(chalk.red(`Error: ${err.message}`)); 4914 | showTagsHelp(); 4915 | process.exit(1); 4916 | }); 4917 | 4918 | // use-tag command 4919 | programInstance 4920 | .command('use-tag') 4921 | .description('Switch to a different tag context') 4922 | .argument('<tagName>', 'Name of the tag to switch to') 4923 | .option( 4924 | '-f, --file <file>', 4925 | 'Path to the tasks file', 4926 | TASKMASTER_TASKS_FILE 4927 | ) 4928 | .action(async (tagName, options) => { 4929 | try { 4930 | // Initialize TaskMaster 4931 | const taskMaster = initTaskMaster({ 4932 | tasksPath: options.file || true 4933 | }); 4934 | const tasksPath = taskMaster.getTasksPath(); 4935 | 4936 | // Validate tasks file exists 4937 | if (!fs.existsSync(tasksPath)) { 4938 | console.error( 4939 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 4940 | ); 4941 | process.exit(1); 4942 | } 4943 | 4944 | const context = { 4945 | projectRoot: taskMaster.getProjectRoot(), 4946 | commandName: 'use-tag', 4947 | outputType: 'cli' 4948 | }; 4949 | 4950 | await useTag(taskMaster.getTasksPath(), tagName, {}, context, 'text'); 4951 | } catch (error) { 4952 | console.error(chalk.red(`Error switching tag: ${error.message}`)); 4953 | showUseTagHelp(); 4954 | process.exit(1); 4955 | } 4956 | }) 4957 | .on('error', function (err) { 4958 | console.error(chalk.red(`Error: ${err.message}`)); 4959 | showUseTagHelp(); 4960 | process.exit(1); 4961 | }); 4962 | 4963 | // rename-tag command 4964 | programInstance 4965 | .command('rename-tag') 4966 | .description('Rename an existing tag') 4967 | .argument('<oldName>', 'Current name of the tag') 4968 | .argument('<newName>', 'New name for the tag') 4969 | .option( 4970 | '-f, --file <file>', 4971 | 'Path to the tasks file', 4972 | TASKMASTER_TASKS_FILE 4973 | ) 4974 | .action(async (oldName, newName, options) => { 4975 | try { 4976 | // Initialize TaskMaster 4977 | const taskMaster = initTaskMaster({ 4978 | tasksPath: options.file || true 4979 | }); 4980 | const tasksPath = taskMaster.getTasksPath(); 4981 | 4982 | // Validate tasks file exists 4983 | if (!fs.existsSync(tasksPath)) { 4984 | console.error( 4985 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 4986 | ); 4987 | process.exit(1); 4988 | } 4989 | 4990 | const context = { 4991 | projectRoot: taskMaster.getProjectRoot(), 4992 | commandName: 'rename-tag', 4993 | outputType: 'cli' 4994 | }; 4995 | 4996 | await renameTag( 4997 | taskMaster.getTasksPath(), 4998 | oldName, 4999 | newName, 5000 | {}, 5001 | context, 5002 | 'text' 5003 | ); 5004 | } catch (error) { 5005 | console.error(chalk.red(`Error renaming tag: ${error.message}`)); 5006 | process.exit(1); 5007 | } 5008 | }) 5009 | .on('error', function (err) { 5010 | console.error(chalk.red(`Error: ${err.message}`)); 5011 | process.exit(1); 5012 | }); 5013 | 5014 | // copy-tag command 5015 | programInstance 5016 | .command('copy-tag') 5017 | .description('Copy an existing tag to create a new tag with the same tasks') 5018 | .argument('<sourceName>', 'Name of the source tag to copy from') 5019 | .argument('<targetName>', 'Name of the new tag to create') 5020 | .option( 5021 | '-f, --file <file>', 5022 | 'Path to the tasks file', 5023 | TASKMASTER_TASKS_FILE 5024 | ) 5025 | .option('-d, --description <text>', 'Optional description for the new tag') 5026 | .action(async (sourceName, targetName, options) => { 5027 | try { 5028 | // Initialize TaskMaster 5029 | const taskMaster = initTaskMaster({ 5030 | tasksPath: options.file || true 5031 | }); 5032 | const tasksPath = taskMaster.getTasksPath(); 5033 | 5034 | // Validate tasks file exists 5035 | if (!fs.existsSync(tasksPath)) { 5036 | console.error( 5037 | chalk.red(`Error: Tasks file not found at path: ${tasksPath}`) 5038 | ); 5039 | process.exit(1); 5040 | } 5041 | 5042 | const copyOptions = { 5043 | description: options.description 5044 | }; 5045 | 5046 | const context = { 5047 | projectRoot: taskMaster.getProjectRoot(), 5048 | commandName: 'copy-tag', 5049 | outputType: 'cli' 5050 | }; 5051 | 5052 | await copyTag( 5053 | tasksPath, 5054 | sourceName, 5055 | targetName, 5056 | copyOptions, 5057 | context, 5058 | 'text' 5059 | ); 5060 | } catch (error) { 5061 | console.error(chalk.red(`Error copying tag: ${error.message}`)); 5062 | process.exit(1); 5063 | } 5064 | }) 5065 | .on('error', function (err) { 5066 | console.error(chalk.red(`Error: ${err.message}`)); 5067 | process.exit(1); 5068 | }); 5069 | 5070 | return programInstance; 5071 | } 5072 | 5073 | /** 5074 | * Setup the CLI application 5075 | * @returns {Object} Configured Commander program 5076 | */ 5077 | function setupCLI() { 5078 | // Create a new program instance 5079 | const programInstance = new Command() 5080 | .name('task-master') 5081 | .description('AI-driven development task management') 5082 | .version(process.env.TM_PUBLIC_VERSION || 'unknown') 5083 | .helpOption('-h, --help', 'Display help') 5084 | .addHelpCommand(false); // Disable default help command 5085 | 5086 | // Only override help for the main program, not for individual commands 5087 | const originalHelpInformation = 5088 | programInstance.helpInformation.bind(programInstance); 5089 | programInstance.helpInformation = function () { 5090 | // If this is being called for a subcommand, use the default Commander.js help 5091 | if (this.parent && this.parent !== programInstance) { 5092 | return originalHelpInformation(); 5093 | } 5094 | // If this is the main program help, use our custom display 5095 | displayHelp(); 5096 | return ''; 5097 | }; 5098 | 5099 | // Register commands 5100 | registerCommands(programInstance); 5101 | 5102 | return programInstance; 5103 | } 5104 | 5105 | /** 5106 | * Parse arguments and run the CLI 5107 | * @param {Array} argv - Command-line arguments 5108 | */ 5109 | async function runCLI(argv = process.argv) { 5110 | try { 5111 | // Display banner if not in a pipe (except for init command which has its own banner) 5112 | const isInitCommand = argv.includes('init'); 5113 | if (process.stdout.isTTY && !isInitCommand) { 5114 | displayBanner(); 5115 | } 5116 | 5117 | // If no arguments provided, show help 5118 | if (argv.length <= 2) { 5119 | displayHelp(); 5120 | process.exit(0); 5121 | } 5122 | 5123 | // Start the update check in the background - don't await yet 5124 | const currentVersion = getTaskMasterVersion(); 5125 | const updateCheckPromise = checkForUpdate(currentVersion); 5126 | 5127 | // Setup and parse 5128 | // NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config 5129 | // This means the ConfigurationError might be thrown here if configuration file is missing. 5130 | const programInstance = setupCLI(); 5131 | await programInstance.parseAsync(argv); 5132 | 5133 | // After command execution, check if an update is available 5134 | const updateInfo = await updateCheckPromise; 5135 | if (updateInfo.needsUpdate) { 5136 | // Display the upgrade notification first 5137 | displayUpgradeNotification( 5138 | updateInfo.currentVersion, 5139 | updateInfo.latestVersion 5140 | ); 5141 | 5142 | // Then automatically perform the update 5143 | const updateSuccess = await performAutoUpdate(updateInfo.latestVersion); 5144 | if (updateSuccess) { 5145 | // Exit gracefully after successful update 5146 | process.exit(0); 5147 | } 5148 | } 5149 | 5150 | // Check if migration has occurred and show FYI notice once 5151 | try { 5152 | // Use initTaskMaster with no required fields - will only fail if no project root 5153 | const taskMaster = initTaskMaster({}); 5154 | 5155 | const tasksPath = taskMaster.getTasksPath(); 5156 | const statePath = taskMaster.getStatePath(); 5157 | 5158 | if (tasksPath && fs.existsSync(tasksPath)) { 5159 | // Read raw file to check if it has master key (bypassing tag resolution) 5160 | const rawData = fs.readFileSync(tasksPath, 'utf8'); 5161 | const parsedData = JSON.parse(rawData); 5162 | 5163 | if (parsedData && parsedData.master) { 5164 | // Migration has occurred, check if we've shown the notice 5165 | let stateData = { migrationNoticeShown: false }; 5166 | if (statePath && fs.existsSync(statePath)) { 5167 | // Read state.json directly without tag resolution since it's not a tagged file 5168 | const rawStateData = fs.readFileSync(statePath, 'utf8'); 5169 | stateData = JSON.parse(rawStateData) || stateData; 5170 | } 5171 | 5172 | if (!stateData.migrationNoticeShown) { 5173 | displayTaggedTasksFYI({ _migrationHappened: true }); 5174 | 5175 | // Mark as shown 5176 | stateData.migrationNoticeShown = true; 5177 | // Write state.json directly without tag resolution since it's not a tagged file 5178 | if (statePath) { 5179 | fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2)); 5180 | } 5181 | } 5182 | } 5183 | } 5184 | } catch (error) { 5185 | // Silently ignore errors checking for migration notice 5186 | } 5187 | } catch (error) { 5188 | // ** Specific catch block for missing configuration file ** 5189 | if (error instanceof ConfigurationError) { 5190 | console.error( 5191 | boxen( 5192 | chalk.red.bold('Configuration Update Required!') + 5193 | '\n\n' + 5194 | chalk.white('Taskmaster now uses a ') + 5195 | chalk.yellow.bold('configuration file') + 5196 | chalk.white( 5197 | ' in your project for AI model choices and settings.\n\n' + 5198 | 'This file appears to be ' 5199 | ) + 5200 | chalk.red.bold('missing') + 5201 | chalk.white('. No worries though.\n\n') + 5202 | chalk.cyan.bold('To create this file, run the interactive setup:') + 5203 | '\n' + 5204 | chalk.green(' task-master models --setup') + 5205 | '\n\n' + 5206 | chalk.white.bold('Key Points:') + 5207 | '\n' + 5208 | chalk.white('* ') + 5209 | chalk.yellow.bold('Configuration file') + 5210 | chalk.white( 5211 | ': Stores your AI model settings (do not manually edit)\n' 5212 | ) + 5213 | chalk.white('* ') + 5214 | chalk.yellow.bold('.env & .mcp.json') + 5215 | chalk.white(': Still used ') + 5216 | chalk.red.bold('only') + 5217 | chalk.white(' for your AI provider API keys.\n\n') + 5218 | chalk.cyan( 5219 | '`task-master models` to check your config & available models\n' 5220 | ) + 5221 | chalk.cyan( 5222 | '`task-master models --setup` to adjust the AI models used by Taskmaster' 5223 | ), 5224 | { 5225 | padding: 1, 5226 | margin: { top: 1 }, 5227 | borderColor: 'red', 5228 | borderStyle: 'round' 5229 | } 5230 | ) 5231 | ); 5232 | } else { 5233 | // Generic error handling for other errors 5234 | console.error(chalk.red(`Error: ${error.message}`)); 5235 | if (getDebugFlag()) { 5236 | console.error(error); 5237 | } 5238 | } 5239 | 5240 | process.exit(1); 5241 | } 5242 | } 5243 | 5244 | /** 5245 | * Resolve the final complexity-report path. 5246 | * Rules: 5247 | * 1. If caller passes --output, always respect it. 5248 | * 2. If no explicit output AND tag === 'master' → default report file 5249 | * 3. If no explicit output AND tag !== 'master' → append _<tag>.json 5250 | * 5251 | * @param {string|undefined} outputOpt --output value from CLI (may be undefined) 5252 | * @param {string} targetTag resolved tag (defaults to 'master') 5253 | * @param {string} projectRoot absolute project root 5254 | * @returns {string} absolute path for the report 5255 | */ 5256 | export function resolveComplexityReportPath({ 5257 | projectRoot, 5258 | tag = 'master', 5259 | output // may be undefined 5260 | }) { 5261 | // 1. user knows best 5262 | if (output) { 5263 | return path.isAbsolute(output) ? output : path.join(projectRoot, output); 5264 | } 5265 | 5266 | // 2. default naming 5267 | const base = path.join(projectRoot, COMPLEXITY_REPORT_FILE); 5268 | return tag !== 'master' ? base.replace('.json', `_${tag}.json`) : base; 5269 | } 5270 | 5271 | export { registerCommands, setupCLI, runCLI }; 5272 | ```