This is page 49 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/ui.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * ui.js 3 | * User interface functions for the Task Master CLI 4 | */ 5 | 6 | import chalk from 'chalk'; 7 | import figlet from 'figlet'; 8 | import boxen from 'boxen'; 9 | import ora from 'ora'; 10 | import Table from 'cli-table3'; 11 | import gradient from 'gradient-string'; 12 | import readline from 'readline'; 13 | import { 14 | log, 15 | findTaskById, 16 | readJSON, 17 | truncate, 18 | isSilentMode, 19 | formatTaskId 20 | } from './utils.js'; 21 | import fs from 'fs'; 22 | import { 23 | findNextTask, 24 | analyzeTaskComplexity, 25 | readComplexityReport 26 | } from './task-manager.js'; 27 | import { getProjectName, getDefaultSubtasks } from './config-manager.js'; 28 | import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; 29 | import { 30 | TASKMASTER_CONFIG_FILE, 31 | TASKMASTER_TASKS_FILE 32 | } from '../../src/constants/paths.js'; 33 | import { getTaskMasterVersion } from '../../src/utils/getVersion.js'; 34 | 35 | // Create a color gradient for the banner 36 | const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']); 37 | const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']); 38 | 39 | /** 40 | * Display FYI notice about tagged task lists (only if migration occurred) 41 | * @param {Object} data - Data object that may contain _migrationHappened flag 42 | */ 43 | function displayTaggedTasksFYI(data) { 44 | if (isSilentMode() || !data || !data._migrationHappened) return; 45 | 46 | console.log( 47 | boxen( 48 | chalk.white.bold('FYI: ') + 49 | chalk.gray('Taskmaster now supports separate task lists per tag. ') + 50 | chalk.cyan( 51 | 'Use the --tag flag to create/read/update/filter tasks by tag.' 52 | ), 53 | { 54 | padding: { top: 0, bottom: 0, left: 2, right: 2 }, 55 | borderColor: 'cyan', 56 | borderStyle: 'round', 57 | margin: { top: 1, bottom: 1 } 58 | } 59 | ) 60 | ); 61 | } 62 | 63 | /** 64 | * Display a small, non-intrusive indicator showing the current tag context 65 | * @param {string} tagName - The tag name to display 66 | * @param {Object} options - Display options 67 | * @param {boolean} [options.skipIfMaster=false] - Don't show indicator if tag is 'master' 68 | * @param {boolean} [options.dim=false] - Use dimmed styling 69 | */ 70 | function displayCurrentTagIndicator(tag, options = {}) { 71 | if (isSilentMode()) return; 72 | 73 | const { skipIfMaster = false, dim = false } = options; 74 | 75 | // Skip display for master tag only if explicitly requested 76 | if (skipIfMaster && tag === 'master') return; 77 | 78 | // Create a small, tasteful tag indicator 79 | const tagIcon = '🏷️'; 80 | const tagText = dim 81 | ? chalk.gray(`${tagIcon} tag: ${tag}`) 82 | : chalk.dim(`${tagIcon} tag: `) + chalk.cyan(tag); 83 | 84 | console.log(tagText); 85 | } 86 | 87 | /** 88 | * Display a fancy banner for the CLI 89 | */ 90 | function displayBanner() { 91 | if (isSilentMode()) return; 92 | 93 | // console.clear(); // Removing this to avoid clearing the terminal per command 94 | const bannerText = figlet.textSync('Task Master', { 95 | font: 'Standard', 96 | horizontalLayout: 'default', 97 | verticalLayout: 'default' 98 | }); 99 | 100 | console.log(coolGradient(bannerText)); 101 | 102 | // Add creator credit line below the banner 103 | console.log( 104 | chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano') 105 | ); 106 | 107 | // Read version directly from package.json 108 | const version = getTaskMasterVersion(); 109 | 110 | console.log( 111 | boxen( 112 | chalk.white( 113 | `${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${getProjectName(null)}` 114 | ), 115 | { 116 | padding: 1, 117 | margin: { top: 0, bottom: 1 }, 118 | borderStyle: 'round', 119 | borderColor: 'cyan' 120 | } 121 | ) 122 | ); 123 | } 124 | 125 | /** 126 | * Start a loading indicator with an animated spinner 127 | * @param {string} message - Message to display next to the spinner 128 | * @returns {Object} Spinner object 129 | */ 130 | function startLoadingIndicator(message) { 131 | if (isSilentMode()) return null; 132 | 133 | const spinner = ora({ 134 | text: message, 135 | color: 'cyan' 136 | }).start(); 137 | 138 | return spinner; 139 | } 140 | 141 | /** 142 | * Stop a loading indicator (basic stop, no success/fail indicator) 143 | * @param {Object} spinner - Spinner object to stop 144 | */ 145 | function stopLoadingIndicator(spinner) { 146 | if (spinner && typeof spinner.stop === 'function') { 147 | spinner.stop(); 148 | } 149 | } 150 | 151 | /** 152 | * Complete a loading indicator with success (shows checkmark) 153 | * @param {Object} spinner - Spinner object to complete 154 | * @param {string} message - Optional success message (defaults to current text) 155 | */ 156 | function succeedLoadingIndicator(spinner, message = null) { 157 | if (spinner && typeof spinner.succeed === 'function') { 158 | if (message) { 159 | spinner.succeed(message); 160 | } else { 161 | spinner.succeed(); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * Complete a loading indicator with failure (shows X) 168 | * @param {Object} spinner - Spinner object to fail 169 | * @param {string} message - Optional failure message (defaults to current text) 170 | */ 171 | function failLoadingIndicator(spinner, message = null) { 172 | if (spinner && typeof spinner.fail === 'function') { 173 | if (message) { 174 | spinner.fail(message); 175 | } else { 176 | spinner.fail(); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * Complete a loading indicator with warning (shows warning symbol) 183 | * @param {Object} spinner - Spinner object to warn 184 | * @param {string} message - Optional warning message (defaults to current text) 185 | */ 186 | function warnLoadingIndicator(spinner, message = null) { 187 | if (spinner && typeof spinner.warn === 'function') { 188 | if (message) { 189 | spinner.warn(message); 190 | } else { 191 | spinner.warn(); 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * Complete a loading indicator with info (shows info symbol) 198 | * @param {Object} spinner - Spinner object to complete with info 199 | * @param {string} message - Optional info message (defaults to current text) 200 | */ 201 | function infoLoadingIndicator(spinner, message = null) { 202 | if (spinner && typeof spinner.info === 'function') { 203 | if (message) { 204 | spinner.info(message); 205 | } else { 206 | spinner.info(); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Create a colored progress bar 213 | * @param {number} percent - The completion percentage 214 | * @param {number} length - The total length of the progress bar in characters 215 | * @param {Object} statusBreakdown - Optional breakdown of non-complete statuses (e.g., {pending: 20, 'in-progress': 10}) 216 | * @returns {string} The formatted progress bar 217 | */ 218 | function createProgressBar(percent, length = 30, statusBreakdown = null) { 219 | // Adjust the percent to treat deferred and cancelled as complete 220 | const effectivePercent = statusBreakdown 221 | ? Math.min( 222 | 100, 223 | percent + 224 | (statusBreakdown.deferred || 0) + 225 | (statusBreakdown.cancelled || 0) 226 | ) 227 | : percent; 228 | 229 | // Calculate how many characters to fill for "true completion" 230 | const trueCompletedFilled = Math.round((percent * length) / 100); 231 | 232 | // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) 233 | const effectiveCompletedFilled = Math.round( 234 | (effectivePercent * length) / 100 235 | ); 236 | 237 | // The "deferred/cancelled" section (difference between true and effective) 238 | const deferredCancelledFilled = 239 | effectiveCompletedFilled - trueCompletedFilled; 240 | 241 | // Set the empty section (remaining after effective completion) 242 | const empty = length - effectiveCompletedFilled; 243 | 244 | // Determine color based on percentage for the completed section 245 | let completedColor; 246 | if (percent < 25) { 247 | completedColor = chalk.red; 248 | } else if (percent < 50) { 249 | completedColor = chalk.hex('#FFA500'); // Orange 250 | } else if (percent < 75) { 251 | completedColor = chalk.yellow; 252 | } else if (percent < 100) { 253 | completedColor = chalk.green; 254 | } else { 255 | completedColor = chalk.hex('#006400'); // Dark green 256 | } 257 | 258 | // Create colored sections 259 | const completedSection = completedColor('█'.repeat(trueCompletedFilled)); 260 | 261 | // Gray section for deferred/cancelled items 262 | const deferredCancelledSection = chalk.gray( 263 | '█'.repeat(deferredCancelledFilled) 264 | ); 265 | 266 | // If we have a status breakdown, create a multi-colored remaining section 267 | let remainingSection = ''; 268 | 269 | if (statusBreakdown && empty > 0) { 270 | // Status colors (matching the statusConfig colors in getStatusWithColor) 271 | const statusColors = { 272 | pending: chalk.yellow, 273 | 'in-progress': chalk.hex('#FFA500'), // Orange 274 | blocked: chalk.red, 275 | review: chalk.magenta 276 | // Deferred and cancelled are treated as part of the completed section 277 | }; 278 | 279 | // Calculate proportions for each status 280 | const totalRemaining = Object.entries(statusBreakdown) 281 | .filter( 282 | ([status]) => 283 | !['deferred', 'cancelled', 'done', 'completed'].includes(status) 284 | ) 285 | .reduce((sum, [_, val]) => sum + val, 0); 286 | 287 | // If no remaining tasks with tracked statuses, just use gray 288 | if (totalRemaining <= 0) { 289 | remainingSection = chalk.gray('░'.repeat(empty)); 290 | } else { 291 | // Track how many characters we've added 292 | let addedChars = 0; 293 | 294 | // Add each status section proportionally 295 | for (const [status, percentage] of Object.entries(statusBreakdown)) { 296 | // Skip statuses that are considered complete 297 | if (['deferred', 'cancelled', 'done', 'completed'].includes(status)) 298 | continue; 299 | 300 | // Calculate how many characters this status should fill 301 | const statusChars = Math.round((percentage / totalRemaining) * empty); 302 | 303 | // Make sure we don't exceed the total length due to rounding 304 | const actualChars = Math.min(statusChars, empty - addedChars); 305 | 306 | // Add colored section for this status 307 | const colorFn = statusColors[status] || chalk.gray; 308 | remainingSection += colorFn('░'.repeat(actualChars)); 309 | 310 | addedChars += actualChars; 311 | } 312 | 313 | // If we have any remaining space due to rounding, fill with gray 314 | if (addedChars < empty) { 315 | remainingSection += chalk.gray('░'.repeat(empty - addedChars)); 316 | } 317 | } 318 | } else { 319 | // Default to gray for the empty section if no breakdown provided 320 | remainingSection = chalk.gray('░'.repeat(empty)); 321 | } 322 | 323 | // Effective percentage text color should reflect the highest category 324 | const percentTextColor = 325 | percent === 100 326 | ? chalk.hex('#006400') // Dark green for 100% 327 | : effectivePercent === 100 328 | ? chalk.gray // Gray for 100% with deferred/cancelled 329 | : completedColor; // Otherwise match the completed color 330 | 331 | // Build the complete progress bar 332 | return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; 333 | } 334 | 335 | /** 336 | * Get a colored status string based on the status value 337 | * @param {string} status - Task status (e.g., "done", "pending", "in-progress") 338 | * @param {boolean} forTable - Whether the status is being displayed in a table 339 | * @returns {string} Colored status string 340 | */ 341 | function getStatusWithColor(status, forTable = false) { 342 | if (!status) { 343 | return chalk.gray('❓ unknown'); 344 | } 345 | 346 | const statusConfig = { 347 | done: { color: chalk.green, icon: '✓', tableIcon: '✓' }, 348 | completed: { color: chalk.green, icon: '✓', tableIcon: '✓' }, 349 | pending: { color: chalk.yellow, icon: '○', tableIcon: '⏱' }, 350 | 'in-progress': { color: chalk.hex('#FFA500'), icon: '🔄', tableIcon: '►' }, 351 | deferred: { color: chalk.gray, icon: 'x', tableIcon: '⏱' }, 352 | blocked: { color: chalk.red, icon: '!', tableIcon: '✗' }, 353 | review: { color: chalk.magenta, icon: '?', tableIcon: '?' }, 354 | cancelled: { color: chalk.gray, icon: '❌', tableIcon: 'x' } 355 | }; 356 | 357 | const config = statusConfig[status.toLowerCase()] || { 358 | color: chalk.red, 359 | icon: '❌', 360 | tableIcon: '✗' 361 | }; 362 | 363 | // Use simpler icons for table display to prevent border issues 364 | if (forTable) { 365 | // Use ASCII characters instead of Unicode for completely stable display 366 | const simpleIcons = { 367 | done: '✓', 368 | completed: '✓', 369 | pending: '○', 370 | 'in-progress': '►', 371 | deferred: 'x', 372 | blocked: '!', // Using plain x character for better compatibility 373 | review: '?' // Using circled dot symbol 374 | }; 375 | const simpleIcon = simpleIcons[status.toLowerCase()] || 'x'; 376 | return config.color(`${simpleIcon} ${status}`); 377 | } 378 | 379 | return config.color(`${config.icon} ${status}`); 380 | } 381 | 382 | /** 383 | * Format dependencies list with status indicators 384 | * @param {Array} dependencies - Array of dependency IDs 385 | * @param {Array} allTasks - Array of all tasks 386 | * @param {boolean} forConsole - Whether the output is for console display 387 | * @param {Object|null} complexityReport - Optional pre-loaded complexity report 388 | * @returns {string} Formatted dependencies string 389 | */ 390 | function formatDependenciesWithStatus( 391 | dependencies, 392 | allTasks, 393 | forConsole = false, 394 | complexityReport = null // Add complexityReport parameter 395 | ) { 396 | if ( 397 | !dependencies || 398 | !Array.isArray(dependencies) || 399 | dependencies.length === 0 400 | ) { 401 | return forConsole ? chalk.gray('None') : 'None'; 402 | } 403 | 404 | const formattedDeps = dependencies.map((depId) => { 405 | const depIdStr = depId.toString(); // Ensure string format for display 406 | 407 | // Check if it's already a fully qualified subtask ID (like "22.1") 408 | if (depIdStr.includes('.')) { 409 | const parts = depIdStr.split('.'); 410 | // Validate that it's a proper subtask format (parentId.subtaskId) 411 | if (parts.length !== 2 || !parts[0] || !parts[1]) { 412 | // Invalid format - treat as regular dependency 413 | const numericDepId = 414 | typeof depId === 'string' ? parseInt(depId, 10) : depId; 415 | const depTaskResult = findTaskById( 416 | allTasks, 417 | numericDepId, 418 | complexityReport 419 | ); 420 | const depTask = depTaskResult.task; 421 | 422 | if (!depTask) { 423 | return forConsole 424 | ? chalk.red(`${depIdStr} (Not found)`) 425 | : `${depIdStr} (Not found)`; 426 | } 427 | 428 | const status = depTask.status || 'pending'; 429 | const isDone = 430 | status.toLowerCase() === 'done' || 431 | status.toLowerCase() === 'completed'; 432 | const isInProgress = status.toLowerCase() === 'in-progress'; 433 | 434 | if (forConsole) { 435 | if (isDone) { 436 | return chalk.green.bold(depIdStr); 437 | } else if (isInProgress) { 438 | return chalk.yellow.bold(depIdStr); 439 | } else { 440 | return chalk.red.bold(depIdStr); 441 | } 442 | } 443 | return depIdStr; 444 | } 445 | 446 | const [parentId, subtaskId] = parts.map((id) => parseInt(id, 10)); 447 | 448 | // Find the parent task 449 | const parentTask = allTasks.find((t) => t.id === parentId); 450 | if (!parentTask || !parentTask.subtasks) { 451 | return forConsole 452 | ? chalk.red(`${depIdStr} (Not found)`) 453 | : `${depIdStr} (Not found)`; 454 | } 455 | 456 | // Find the subtask 457 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); 458 | if (!subtask) { 459 | return forConsole 460 | ? chalk.red(`${depIdStr} (Not found)`) 461 | : `${depIdStr} (Not found)`; 462 | } 463 | 464 | // Format with status 465 | const status = subtask.status || 'pending'; 466 | const isDone = 467 | status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; 468 | const isInProgress = status.toLowerCase() === 'in-progress'; 469 | 470 | if (forConsole) { 471 | if (isDone) { 472 | return chalk.green.bold(depIdStr); 473 | } else if (isInProgress) { 474 | return chalk.hex('#FFA500').bold(depIdStr); 475 | } else { 476 | return chalk.red.bold(depIdStr); 477 | } 478 | } 479 | 480 | // For plain text output (task files), return just the ID without any formatting or emoji 481 | return depIdStr; 482 | } 483 | 484 | // If depId is a number less than 100, it's likely a reference to a subtask ID in the current task 485 | // This case is typically handled elsewhere (in task-specific code) before calling this function 486 | 487 | // For regular task dependencies (not subtasks) 488 | // Convert string depId to number if needed 489 | const numericDepId = 490 | typeof depId === 'string' ? parseInt(depId, 10) : depId; 491 | 492 | // Look up the task using the numeric ID 493 | const depTaskResult = findTaskById( 494 | allTasks, 495 | numericDepId, 496 | complexityReport 497 | ); 498 | const depTask = depTaskResult.task; // Access the task object from the result 499 | 500 | if (!depTask) { 501 | return forConsole 502 | ? chalk.red(`${depIdStr} (Not found)`) 503 | : `${depIdStr} (Not found)`; 504 | } 505 | 506 | // Format with status 507 | const status = depTask.status || 'pending'; 508 | const isDone = 509 | status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; 510 | const isInProgress = status.toLowerCase() === 'in-progress'; 511 | 512 | if (forConsole) { 513 | if (isDone) { 514 | return chalk.green.bold(depIdStr); 515 | } else if (isInProgress) { 516 | return chalk.yellow.bold(depIdStr); 517 | } else { 518 | return chalk.red.bold(depIdStr); 519 | } 520 | } 521 | 522 | // For plain text output (task files), return just the ID without any formatting or emoji 523 | return depIdStr; 524 | }); 525 | 526 | return formattedDeps.join(', '); 527 | } 528 | 529 | /** 530 | * Display a comprehensive help guide 531 | */ 532 | function displayHelp() { 533 | // Get terminal width - moved to top of function to make it available throughout 534 | const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect 535 | 536 | console.log( 537 | boxen(chalk.white.bold('Task Master CLI'), { 538 | padding: 1, 539 | borderColor: 'blue', 540 | borderStyle: 'round', 541 | margin: { top: 1, bottom: 1 } 542 | }) 543 | ); 544 | 545 | // Command categories 546 | const commandCategories = [ 547 | { 548 | title: 'Project Setup & Configuration', 549 | color: 'blue', 550 | commands: [ 551 | { 552 | name: 'init', 553 | args: '[--name=<name>] [--description=<desc>] [-y]', 554 | desc: 'Initialize a new project with Task Master structure' 555 | }, 556 | { 557 | name: 'models', 558 | args: '', 559 | desc: 'View current AI model configuration and available models' 560 | }, 561 | { 562 | name: 'models --setup', 563 | args: '', 564 | desc: 'Run interactive setup to configure AI models' 565 | }, 566 | { 567 | name: 'models --set-main', 568 | args: '<model_id>', 569 | desc: 'Set the primary model for task generation' 570 | }, 571 | { 572 | name: 'models --set-research', 573 | args: '<model_id>', 574 | desc: 'Set the model for research operations' 575 | }, 576 | { 577 | name: 'models --set-fallback', 578 | args: '<model_id>', 579 | desc: 'Set the fallback model (optional)' 580 | } 581 | ] 582 | }, 583 | { 584 | title: 'Task Generation', 585 | color: 'cyan', 586 | commands: [ 587 | { 588 | name: 'parse-prd', 589 | args: '--input=<file.txt> [--num-tasks=10]', 590 | desc: 'Generate tasks from a PRD document' 591 | }, 592 | { 593 | name: 'generate', 594 | args: '', 595 | desc: 'Create individual task files from tasks.json' 596 | } 597 | ] 598 | }, 599 | { 600 | title: 'Task Management', 601 | color: 'green', 602 | commands: [ 603 | { 604 | name: 'list', 605 | args: '[--status=<status>] [--with-subtasks]', 606 | desc: 'List all tasks with their status' 607 | }, 608 | { 609 | name: 'set-status', 610 | args: '--id=<id> --status=<status>', 611 | desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})` 612 | }, 613 | { 614 | name: 'sync-readme', 615 | args: '[--with-subtasks] [--status=<status>]', 616 | desc: 'Export tasks to README.md with professional formatting' 617 | }, 618 | { 619 | name: 'update', 620 | args: '--from=<id> --prompt="<context>"', 621 | desc: 'Update multiple tasks based on new requirements' 622 | }, 623 | { 624 | name: 'update-task', 625 | args: '--id=<id> --prompt="<context>"', 626 | desc: 'Update a single specific task with new information' 627 | }, 628 | { 629 | name: 'update-subtask', 630 | args: '--id=<parentId.subtaskId> --prompt="<context>"', 631 | desc: 'Append additional information to a subtask' 632 | }, 633 | { 634 | name: 'add-task', 635 | args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]', 636 | desc: 'Add a new task using AI' 637 | }, 638 | { 639 | name: 'remove-task', 640 | args: '--id=<id> [-y]', 641 | desc: 'Permanently remove a task or subtask' 642 | } 643 | ] 644 | }, 645 | { 646 | title: 'Subtask Management', 647 | color: 'yellow', 648 | commands: [ 649 | { 650 | name: 'add-subtask', 651 | args: '--parent=<id> --title="<title>" [--description="<desc>"]', 652 | desc: 'Add a new subtask to a parent task' 653 | }, 654 | { 655 | name: 'add-subtask', 656 | args: '--parent=<id> --task-id=<id>', 657 | desc: 'Convert an existing task into a subtask' 658 | }, 659 | { 660 | name: 'remove-subtask', 661 | args: '--id=<parentId.subtaskId> [--convert]', 662 | desc: 'Remove a subtask (optionally convert to standalone task)' 663 | }, 664 | { 665 | name: 'clear-subtasks', 666 | args: '--id=<id>', 667 | desc: 'Remove all subtasks from specified tasks' 668 | }, 669 | { 670 | name: 'clear-subtasks --all', 671 | args: '', 672 | desc: 'Remove subtasks from all tasks' 673 | } 674 | ] 675 | }, 676 | { 677 | title: 'Task Analysis & Breakdown', 678 | color: 'magenta', 679 | commands: [ 680 | { 681 | name: 'analyze-complexity', 682 | args: '[--research] [--threshold=5]', 683 | desc: 'Analyze tasks and generate expansion recommendations' 684 | }, 685 | { 686 | name: 'complexity-report', 687 | args: '[--file=<path>]', 688 | desc: 'Display the complexity analysis report' 689 | }, 690 | { 691 | name: 'expand', 692 | args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]', 693 | desc: 'Break down tasks into detailed subtasks' 694 | }, 695 | { 696 | name: 'expand --all', 697 | args: '[--force] [--research]', 698 | desc: 'Expand all pending tasks with subtasks' 699 | }, 700 | { 701 | name: 'research', 702 | args: '"<prompt>" [-i=<task_ids>] [-f=<file_paths>] [-c="<context>"] [--tree] [-s=<save_file>] [-d=<detail_level>]', 703 | desc: 'Perform AI-powered research queries with project context' 704 | } 705 | ] 706 | }, 707 | { 708 | title: 'Task Navigation & Viewing', 709 | color: 'cyan', 710 | commands: [ 711 | { 712 | name: 'next', 713 | args: '', 714 | desc: 'Show the next task to work on based on dependencies' 715 | }, 716 | { 717 | name: 'show', 718 | args: '<id>', 719 | desc: 'Display detailed information about a specific task' 720 | } 721 | ] 722 | }, 723 | { 724 | title: 'Tag Management', 725 | color: 'magenta', 726 | commands: [ 727 | { 728 | name: 'tags', 729 | args: '[--show-metadata]', 730 | desc: 'List all available tags with task counts' 731 | }, 732 | { 733 | name: 'add-tag', 734 | args: '<tagName> [--copy-from-current] [--copy-from=<tag>] [-d="<desc>"]', 735 | desc: 'Create a new tag context for organizing tasks' 736 | }, 737 | { 738 | name: 'use-tag', 739 | args: '<tagName>', 740 | desc: 'Switch to a different tag context' 741 | }, 742 | { 743 | name: 'delete-tag', 744 | args: '<tagName> [--yes]', 745 | desc: 'Delete an existing tag and all its tasks' 746 | }, 747 | { 748 | name: 'rename-tag', 749 | args: '<oldName> <newName>', 750 | desc: 'Rename an existing tag' 751 | }, 752 | { 753 | name: 'copy-tag', 754 | args: '<sourceName> <targetName> [-d="<desc>"]', 755 | desc: 'Copy an existing tag to create a new tag with the same tasks' 756 | } 757 | ] 758 | }, 759 | { 760 | title: 'Dependency Management', 761 | color: 'blue', 762 | commands: [ 763 | { 764 | name: 'add-dependency', 765 | args: '--id=<id> --depends-on=<id>', 766 | desc: 'Add a dependency to a task' 767 | }, 768 | { 769 | name: 'remove-dependency', 770 | args: '--id=<id> --depends-on=<id>', 771 | desc: 'Remove a dependency from a task' 772 | }, 773 | { 774 | name: 'validate-dependencies', 775 | args: '', 776 | desc: 'Identify invalid dependencies without fixing them' 777 | }, 778 | { 779 | name: 'fix-dependencies', 780 | args: '', 781 | desc: 'Fix invalid dependencies automatically' 782 | } 783 | ] 784 | } 785 | ]; 786 | 787 | // Display each category 788 | commandCategories.forEach((category) => { 789 | console.log( 790 | boxen(chalk[category.color].bold(category.title), { 791 | padding: { left: 2, right: 2, top: 0, bottom: 0 }, 792 | margin: { top: 1, bottom: 0 }, 793 | borderColor: category.color, 794 | borderStyle: 'round' 795 | }) 796 | ); 797 | 798 | // Calculate dynamic column widths - adjust ratios as needed 799 | const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25 800 | const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40 801 | const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer 802 | 803 | const commandTable = new Table({ 804 | colWidths: [nameWidth, argsWidth, descWidth], 805 | chars: { 806 | top: '', 807 | 'top-mid': '', 808 | 'top-left': '', 809 | 'top-right': '', 810 | bottom: '', 811 | 'bottom-mid': '', 812 | 'bottom-left': '', 813 | 'bottom-right': '', 814 | left: '', 815 | 'left-mid': '', 816 | mid: '', 817 | 'mid-mid': '', 818 | right: '', 819 | 'right-mid': '', 820 | middle: ' ' 821 | }, 822 | style: { border: [], 'padding-left': 4 }, 823 | wordWrap: true 824 | }); 825 | 826 | category.commands.forEach((cmd, index) => { 827 | commandTable.push([ 828 | `${chalk.yellow.bold(cmd.name)}${chalk.reset('')}`, 829 | `${chalk.white(cmd.args)}${chalk.reset('')}`, 830 | `${chalk.dim(cmd.desc)}${chalk.reset('')}` 831 | ]); 832 | }); 833 | 834 | console.log(commandTable.toString()); 835 | console.log(''); 836 | }); 837 | 838 | // Display configuration section 839 | console.log( 840 | boxen(chalk.cyan.bold('Configuration'), { 841 | padding: { left: 2, right: 2, top: 0, bottom: 0 }, 842 | margin: { top: 1, bottom: 0 }, 843 | borderColor: 'cyan', 844 | borderStyle: 'round' 845 | }) 846 | ); 847 | 848 | // Get terminal width if not already defined 849 | const configTerminalWidth = terminalWidth || process.stdout.columns || 100; 850 | 851 | // Calculate dynamic column widths for config table 852 | const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25)); 853 | const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45)); 854 | const configValueWidth = Math.max( 855 | 30, 856 | Math.floor(configTerminalWidth * 0.3) - 10 857 | ); 858 | 859 | const configTable = new Table({ 860 | colWidths: [configKeyWidth, configDescWidth, configValueWidth], 861 | chars: { 862 | top: '', 863 | 'top-mid': '', 864 | 'top-left': '', 865 | 'top-right': '', 866 | bottom: '', 867 | 'bottom-mid': '', 868 | 'bottom-left': '', 869 | 'bottom-right': '', 870 | left: '', 871 | 'left-mid': '', 872 | mid: '', 873 | 'mid-mid': '', 874 | right: '', 875 | 'right-mid': '', 876 | middle: ' ' 877 | }, 878 | style: { border: [], 'padding-left': 4 }, 879 | wordWrap: true 880 | }); 881 | 882 | configTable.push( 883 | [ 884 | `${chalk.yellow(TASKMASTER_CONFIG_FILE)}${chalk.reset('')}`, 885 | `${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`, 886 | `${chalk.dim('Managed by models cmd')}${chalk.reset('')}` 887 | ], 888 | [ 889 | `${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`, 890 | `${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`, 891 | `${chalk.dim('Required in .env file')}${chalk.reset('')}` 892 | ], 893 | [ 894 | `${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`, 895 | `${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`, 896 | `${chalk.dim('Required in .cursor/')}${chalk.reset('')}` 897 | ] 898 | ); 899 | 900 | console.log(configTable.toString()); 901 | console.log(''); 902 | 903 | // Show helpful hints 904 | console.log( 905 | boxen( 906 | chalk.white.bold('Quick Start:') + 907 | '\n\n' + 908 | chalk.cyan('1. Create Project: ') + 909 | chalk.white('task-master init') + 910 | '\n' + 911 | chalk.cyan('2. Setup Models: ') + 912 | chalk.white('task-master models --setup') + 913 | '\n' + 914 | chalk.cyan('3. Parse PRD: ') + 915 | chalk.white('task-master parse-prd --input=<prd-file>') + 916 | '\n' + 917 | chalk.cyan('4. List Tasks: ') + 918 | chalk.white('task-master list') + 919 | '\n' + 920 | chalk.cyan('5. Find Next Task: ') + 921 | chalk.white('task-master next'), 922 | { 923 | padding: 1, 924 | borderColor: 'yellow', 925 | borderStyle: 'round', 926 | margin: { top: 1 }, 927 | width: Math.min(configTerminalWidth - 10, 100) // Limit width to terminal width minus padding, max 100 928 | } 929 | ) 930 | ); 931 | } 932 | 933 | /** 934 | * Get colored complexity score 935 | * @param {number} score - Complexity score (1-10) 936 | * @returns {string} Colored complexity score 937 | */ 938 | function getComplexityWithColor(score) { 939 | if (score <= 3) return chalk.green(`● ${score}`); 940 | if (score <= 6) return chalk.yellow(`● ${score}`); 941 | return chalk.red(`● ${score}`); 942 | } 943 | 944 | /** 945 | * Truncate a string to a maximum length and add ellipsis if needed 946 | * @param {string} str - The string to truncate 947 | * @param {number} maxLength - Maximum length 948 | * @returns {string} Truncated string 949 | */ 950 | function truncateString(str, maxLength) { 951 | if (!str) return ''; 952 | if (str.length <= maxLength) return str; 953 | return str.substring(0, maxLength - 3) + '...'; 954 | } 955 | 956 | /** 957 | * Display the next task to work on 958 | * @param {string} tasksPath - Path to the tasks.json file 959 | * @param {string} complexityReportPath - Path to the complexity report file 960 | * @param {string} tag - Optional tag to override current tag resolution 961 | */ 962 | async function displayNextTask( 963 | tasksPath, 964 | complexityReportPath = null, 965 | context = {} 966 | ) { 967 | // Extract parameters from context 968 | const { projectRoot, tag } = context; 969 | 970 | // Read the tasks file with proper projectRoot for tag resolution 971 | const data = readJSON(tasksPath, projectRoot, tag); 972 | if (!data || !data.tasks) { 973 | log('error', 'No valid tasks found.'); 974 | process.exit(1); 975 | } 976 | 977 | // Read complexity report once 978 | const complexityReport = readComplexityReport(complexityReportPath); 979 | 980 | // Find the next task 981 | const nextTask = findNextTask(data.tasks, complexityReport); 982 | 983 | if (!nextTask) { 984 | console.log( 985 | boxen( 986 | chalk.yellow('No eligible tasks found!\n\n') + 987 | 'All pending tasks have unsatisfied dependencies, or all tasks are completed.', 988 | { 989 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 990 | borderColor: 'yellow', 991 | borderStyle: 'round', 992 | margin: { top: 1 } 993 | } 994 | ) 995 | ); 996 | return; 997 | } 998 | 999 | // Display the task in a nice format 1000 | console.log( 1001 | boxen(chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`), { 1002 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1003 | borderColor: 'blue', 1004 | borderStyle: 'round', 1005 | margin: { top: 1, bottom: 0 } 1006 | }) 1007 | ); 1008 | 1009 | // Create a table with task details 1010 | const taskTable = new Table({ 1011 | style: { 1012 | head: [], 1013 | border: [], 1014 | 'padding-top': 0, 1015 | 'padding-bottom': 0, 1016 | compact: true 1017 | }, 1018 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 1019 | colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], 1020 | wordWrap: true 1021 | }); 1022 | 1023 | // Priority with color 1024 | const priorityColors = { 1025 | high: chalk.red.bold, 1026 | medium: chalk.yellow, 1027 | low: chalk.gray 1028 | }; 1029 | const priorityColor = 1030 | priorityColors[nextTask.priority || 'medium'] || chalk.white; 1031 | 1032 | // Add task details to table 1033 | taskTable.push( 1034 | [chalk.cyan.bold('ID:'), nextTask.id.toString()], 1035 | [chalk.cyan.bold('Title:'), nextTask.title], 1036 | [ 1037 | chalk.cyan.bold('Priority:'), 1038 | priorityColor(nextTask.priority || 'medium') 1039 | ], 1040 | [ 1041 | chalk.cyan.bold('Dependencies:'), 1042 | formatDependenciesWithStatus( 1043 | nextTask.dependencies, 1044 | data.tasks, 1045 | true, 1046 | complexityReport 1047 | ) 1048 | ], 1049 | [ 1050 | chalk.cyan.bold('Complexity:'), 1051 | nextTask.complexityScore 1052 | ? getComplexityWithColor(nextTask.complexityScore) 1053 | : chalk.gray('N/A') 1054 | ], 1055 | [chalk.cyan.bold('Description:'), nextTask.description] 1056 | ); 1057 | 1058 | console.log(taskTable.toString()); 1059 | 1060 | // If task has details, show them in a separate box 1061 | if (nextTask.details && nextTask.details.trim().length > 0) { 1062 | console.log( 1063 | boxen( 1064 | chalk.white.bold('Implementation Details:') + '\n\n' + nextTask.details, 1065 | { 1066 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1067 | borderColor: 'cyan', 1068 | borderStyle: 'round', 1069 | margin: { top: 1, bottom: 0 } 1070 | } 1071 | ) 1072 | ); 1073 | } 1074 | 1075 | // Determine if the nextTask is a subtask 1076 | const isSubtask = !!nextTask.parentId; 1077 | 1078 | // Show subtasks if they exist (only for parent tasks) 1079 | if (!isSubtask && nextTask.subtasks && nextTask.subtasks.length > 0) { 1080 | console.log( 1081 | boxen(chalk.white.bold('Subtasks'), { 1082 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1083 | margin: { top: 1, bottom: 0 }, 1084 | borderColor: 'magenta', 1085 | borderStyle: 'round' 1086 | }) 1087 | ); 1088 | 1089 | // Calculate available width for the subtask table 1090 | const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect 1091 | 1092 | // Define percentage-based column widths 1093 | const idWidthPct = 8; 1094 | const statusWidthPct = 15; 1095 | const depsWidthPct = 25; 1096 | const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; 1097 | 1098 | // Calculate actual column widths 1099 | const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); 1100 | const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); 1101 | const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); 1102 | const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); 1103 | 1104 | // Create a table for subtasks with improved handling 1105 | const subtaskTable = new Table({ 1106 | head: [ 1107 | chalk.magenta.bold('ID'), 1108 | chalk.magenta.bold('Status'), 1109 | chalk.magenta.bold('Title'), 1110 | chalk.magenta.bold('Deps') 1111 | ], 1112 | colWidths: [idWidth, statusWidth, titleWidth, depsWidth], 1113 | style: { 1114 | head: [], 1115 | border: [], 1116 | 'padding-top': 0, 1117 | 'padding-bottom': 0, 1118 | compact: true 1119 | }, 1120 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 1121 | wordWrap: true 1122 | }); 1123 | 1124 | // Add subtasks to table 1125 | nextTask.subtasks.forEach((st) => { 1126 | const statusColor = 1127 | { 1128 | done: chalk.green, 1129 | completed: chalk.green, 1130 | pending: chalk.yellow, 1131 | 'in-progress': chalk.blue 1132 | }[st.status || 'pending'] || chalk.white; 1133 | 1134 | // Format subtask dependencies 1135 | let subtaskDeps = 'None'; 1136 | if (st.dependencies && st.dependencies.length > 0) { 1137 | // Format dependencies with correct notation 1138 | const formattedDeps = st.dependencies.map((depId) => { 1139 | if (typeof depId === 'number' && depId < 100) { 1140 | const foundSubtask = nextTask.subtasks.find( 1141 | (st) => st.id === depId 1142 | ); 1143 | if (foundSubtask) { 1144 | const isDone = 1145 | foundSubtask.status === 'done' || 1146 | foundSubtask.status === 'completed'; 1147 | const isInProgress = foundSubtask.status === 'in-progress'; 1148 | 1149 | // Use consistent color formatting instead of emojis 1150 | if (isDone) { 1151 | return chalk.green.bold(`${nextTask.id}.${depId}`); 1152 | } else if (isInProgress) { 1153 | return chalk.hex('#FFA500').bold(`${nextTask.id}.${depId}`); 1154 | } else { 1155 | return chalk.red.bold(`${nextTask.id}.${depId}`); 1156 | } 1157 | } 1158 | return chalk.red(`${nextTask.id}.${depId} (Not found)`); 1159 | } 1160 | return depId; 1161 | }); 1162 | 1163 | // Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again 1164 | subtaskDeps = 1165 | formattedDeps.length === 1 1166 | ? formattedDeps[0] 1167 | : formattedDeps.join(chalk.white(', ')); 1168 | } 1169 | 1170 | subtaskTable.push([ 1171 | `${nextTask.id}.${st.id}`, 1172 | statusColor(st.status || 'pending'), 1173 | st.title, 1174 | subtaskDeps 1175 | ]); 1176 | }); 1177 | 1178 | console.log(subtaskTable.toString()); 1179 | } 1180 | 1181 | // Suggest expanding if no subtasks (only for parent tasks without subtasks) 1182 | if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) { 1183 | console.log( 1184 | boxen( 1185 | chalk.yellow('No subtasks found. Consider breaking down this task:') + 1186 | '\n' + 1187 | chalk.white( 1188 | `Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}` 1189 | ), 1190 | { 1191 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1192 | borderColor: 'yellow', 1193 | borderStyle: 'round', 1194 | margin: { top: 1, bottom: 0 } 1195 | } 1196 | ) 1197 | ); 1198 | } 1199 | 1200 | // Show action suggestions 1201 | let suggestedActionsContent = chalk.white.bold('Suggested Actions:') + '\n'; 1202 | if (isSubtask) { 1203 | // Suggested actions for a subtask 1204 | suggestedActionsContent += 1205 | `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + 1206 | `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + 1207 | `${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`; 1208 | } else { 1209 | // Suggested actions for a parent task 1210 | suggestedActionsContent += 1211 | `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` + 1212 | `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` + 1213 | (nextTask.subtasks && nextTask.subtasks.length > 0 1214 | ? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}` // Example: first subtask 1215 | : `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`); 1216 | } 1217 | 1218 | console.log( 1219 | boxen(suggestedActionsContent, { 1220 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1221 | borderColor: 'green', 1222 | borderStyle: 'round', 1223 | margin: { top: 1 } 1224 | }) 1225 | ); 1226 | 1227 | // Show FYI notice if migration occurred 1228 | displayTaggedTasksFYI(data); 1229 | } 1230 | 1231 | /** 1232 | * Display a specific task by ID 1233 | * @param {string} tasksPath - Path to the tasks.json file 1234 | * @param {string|number} taskId - The ID of the task to display 1235 | * @param {string} complexityReportPath - Path to the complexity report file 1236 | * @param {string} [statusFilter] - Optional status to filter subtasks by 1237 | * @param {object} context - Context object containing projectRoot and tag 1238 | * @param {string} context.projectRoot - Project root path 1239 | * @param {string} context.tag - Tag for the task 1240 | */ 1241 | async function displayTaskById( 1242 | tasksPath, 1243 | taskId, 1244 | complexityReportPath = null, 1245 | statusFilter = null, 1246 | context = {} 1247 | ) { 1248 | const { projectRoot, tag } = context; 1249 | 1250 | // Read the tasks file with proper projectRoot for tag resolution 1251 | const data = readJSON(tasksPath, projectRoot, tag); 1252 | if (!data || !data.tasks) { 1253 | log('error', 'No valid tasks found.'); 1254 | process.exit(1); 1255 | } 1256 | 1257 | // Read complexity report once 1258 | const complexityReport = readComplexityReport(complexityReportPath); 1259 | 1260 | // Find the task by ID, applying the status filter if provided 1261 | // Returns { task, originalSubtaskCount, originalSubtasks } 1262 | const { task, originalSubtaskCount, originalSubtasks } = findTaskById( 1263 | data.tasks, 1264 | taskId, 1265 | complexityReport, 1266 | statusFilter 1267 | ); 1268 | 1269 | if (!task) { 1270 | console.log( 1271 | boxen(chalk.yellow(`Task with ID ${taskId} not found!`), { 1272 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1273 | borderColor: 'yellow', 1274 | borderStyle: 'round', 1275 | margin: { top: 1 } 1276 | }) 1277 | ); 1278 | return; 1279 | } 1280 | 1281 | // Handle subtask display specially (This logic remains the same) 1282 | if (task.isSubtask || task.parentTask) { 1283 | console.log( 1284 | boxen( 1285 | chalk.white.bold( 1286 | `Subtask: #${task.parentTask.id}.${task.id} - ${task.title}` 1287 | ), 1288 | { 1289 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1290 | borderColor: 'magenta', 1291 | borderStyle: 'round', 1292 | margin: { top: 1, bottom: 0 } 1293 | } 1294 | ) 1295 | ); 1296 | 1297 | const subtaskTable = new Table({ 1298 | style: { 1299 | head: [], 1300 | border: [], 1301 | 'padding-top': 0, 1302 | 'padding-bottom': 0, 1303 | compact: true 1304 | }, 1305 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 1306 | colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], 1307 | wordWrap: true 1308 | }); 1309 | subtaskTable.push( 1310 | [chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`], 1311 | [ 1312 | chalk.cyan.bold('Parent Task:'), 1313 | `#${task.parentTask.id} - ${task.parentTask.title}` 1314 | ], 1315 | [chalk.cyan.bold('Title:'), task.title], 1316 | [ 1317 | chalk.cyan.bold('Status:'), 1318 | getStatusWithColor(task.status || 'pending', true) 1319 | ], 1320 | [ 1321 | chalk.cyan.bold('Complexity:'), 1322 | task.complexityScore 1323 | ? getComplexityWithColor(task.complexityScore) 1324 | : chalk.gray('N/A') 1325 | ], 1326 | [ 1327 | chalk.cyan.bold('Description:'), 1328 | task.description || 'No description provided.' 1329 | ] 1330 | ); 1331 | console.log(subtaskTable.toString()); 1332 | 1333 | if (task.details && task.details.trim().length > 0) { 1334 | console.log( 1335 | boxen( 1336 | chalk.white.bold('Implementation Details:') + '\n\n' + task.details, 1337 | { 1338 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1339 | borderColor: 'cyan', 1340 | borderStyle: 'round', 1341 | margin: { top: 1, bottom: 0 } 1342 | } 1343 | ) 1344 | ); 1345 | } 1346 | 1347 | console.log( 1348 | boxen( 1349 | chalk.white.bold('Suggested Actions:') + 1350 | '\n' + 1351 | `${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` + 1352 | `${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` + 1353 | `${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`, 1354 | { 1355 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1356 | borderColor: 'green', 1357 | borderStyle: 'round', 1358 | margin: { top: 1 } 1359 | } 1360 | ) 1361 | ); 1362 | return; // Exit after displaying subtask details 1363 | } 1364 | 1365 | // --- Display Regular Task Details --- 1366 | console.log( 1367 | boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), { 1368 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1369 | borderColor: 'blue', 1370 | borderStyle: 'round', 1371 | margin: { top: 1, bottom: 0 } 1372 | }) 1373 | ); 1374 | 1375 | const taskTable = new Table({ 1376 | style: { 1377 | head: [], 1378 | border: [], 1379 | 'padding-top': 0, 1380 | 'padding-bottom': 0, 1381 | compact: true 1382 | }, 1383 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 1384 | colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], 1385 | wordWrap: true 1386 | }); 1387 | const priorityColors = { 1388 | high: chalk.red.bold, 1389 | medium: chalk.yellow, 1390 | low: chalk.gray 1391 | }; 1392 | const priorityColor = 1393 | priorityColors[task.priority || 'medium'] || chalk.white; 1394 | taskTable.push( 1395 | [chalk.cyan.bold('ID:'), task.id.toString()], 1396 | [chalk.cyan.bold('Title:'), task.title], 1397 | [ 1398 | chalk.cyan.bold('Status:'), 1399 | getStatusWithColor(task.status || 'pending', true) 1400 | ], 1401 | [chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')], 1402 | [ 1403 | chalk.cyan.bold('Dependencies:'), 1404 | formatDependenciesWithStatus( 1405 | task.dependencies, 1406 | data.tasks, 1407 | true, 1408 | complexityReport 1409 | ) 1410 | ], 1411 | [ 1412 | chalk.cyan.bold('Complexity:'), 1413 | task.complexityScore 1414 | ? getComplexityWithColor(task.complexityScore) 1415 | : chalk.gray('N/A') 1416 | ], 1417 | [chalk.cyan.bold('Description:'), task.description] 1418 | ); 1419 | console.log(taskTable.toString()); 1420 | 1421 | if (task.details && task.details.trim().length > 0) { 1422 | console.log( 1423 | boxen( 1424 | chalk.white.bold('Implementation Details:') + '\n\n' + task.details, 1425 | { 1426 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1427 | borderColor: 'cyan', 1428 | borderStyle: 'round', 1429 | margin: { top: 1, bottom: 0 } 1430 | } 1431 | ) 1432 | ); 1433 | } 1434 | if (task.testStrategy && task.testStrategy.trim().length > 0) { 1435 | console.log( 1436 | boxen(chalk.white.bold('Test Strategy:') + '\n\n' + task.testStrategy, { 1437 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1438 | borderColor: 'cyan', 1439 | borderStyle: 'round', 1440 | margin: { top: 1, bottom: 0 } 1441 | }) 1442 | ); 1443 | } 1444 | 1445 | // --- Subtask Table Display (uses filtered list: task.subtasks) --- 1446 | if (task.subtasks && task.subtasks.length > 0) { 1447 | console.log( 1448 | boxen(chalk.white.bold('Subtasks'), { 1449 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1450 | margin: { top: 1, bottom: 0 }, 1451 | borderColor: 'magenta', 1452 | borderStyle: 'round' 1453 | }) 1454 | ); 1455 | 1456 | const availableWidth = process.stdout.columns - 10 || 100; 1457 | const idWidthPct = 10; 1458 | const statusWidthPct = 15; 1459 | const depsWidthPct = 25; 1460 | const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; 1461 | const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); 1462 | const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); 1463 | const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); 1464 | const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); 1465 | 1466 | const subtaskTable = new Table({ 1467 | head: [ 1468 | chalk.magenta.bold('ID'), 1469 | chalk.magenta.bold('Status'), 1470 | chalk.magenta.bold('Title'), 1471 | chalk.magenta.bold('Deps') 1472 | ], 1473 | colWidths: [idWidth, statusWidth, titleWidth, depsWidth], 1474 | style: { 1475 | head: [], 1476 | border: [], 1477 | 'padding-top': 0, 1478 | 'padding-bottom': 0, 1479 | compact: true 1480 | }, 1481 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 1482 | wordWrap: true 1483 | }); 1484 | 1485 | // Populate table with the potentially filtered subtasks 1486 | task.subtasks.forEach((st) => { 1487 | const statusColorMap = { 1488 | done: chalk.green, 1489 | completed: chalk.green, 1490 | pending: chalk.yellow, 1491 | 'in-progress': chalk.blue 1492 | }; 1493 | const statusColor = statusColorMap[st.status || 'pending'] || chalk.white; 1494 | let subtaskDeps = 'None'; 1495 | if (st.dependencies && st.dependencies.length > 0) { 1496 | const formattedDeps = st.dependencies.map((depId) => { 1497 | // Use the original, unfiltered list for dependency status lookup 1498 | const sourceListForDeps = originalSubtasks || task.subtasks; 1499 | const foundDepSubtask = 1500 | typeof depId === 'number' && depId < 100 1501 | ? sourceListForDeps.find((sub) => sub.id === depId) 1502 | : null; 1503 | 1504 | if (foundDepSubtask) { 1505 | const isDone = 1506 | foundDepSubtask.status === 'done' || 1507 | foundDepSubtask.status === 'completed'; 1508 | const isInProgress = foundDepSubtask.status === 'in-progress'; 1509 | const color = isDone 1510 | ? chalk.green.bold 1511 | : isInProgress 1512 | ? chalk.hex('#FFA500').bold 1513 | : chalk.red.bold; 1514 | return color(`${task.id}.${depId}`); 1515 | } else if (typeof depId === 'number' && depId < 100) { 1516 | return chalk.red(`${task.id}.${depId} (Not found)`); 1517 | } 1518 | return depId; // Assume it's a top-level task ID if not a number < 100 1519 | }); 1520 | subtaskDeps = 1521 | formattedDeps.length === 1 1522 | ? formattedDeps[0] 1523 | : formattedDeps.join(chalk.white(', ')); 1524 | } 1525 | subtaskTable.push([ 1526 | `${task.id}.${st.id}`, 1527 | statusColor(st.status || 'pending'), 1528 | st.title, 1529 | subtaskDeps 1530 | ]); 1531 | }); 1532 | console.log(subtaskTable.toString()); 1533 | 1534 | // Display filter summary line *immediately after the table* if a filter was applied 1535 | if (statusFilter && originalSubtaskCount !== null) { 1536 | console.log( 1537 | chalk.cyan( 1538 | ` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.` 1539 | ) 1540 | ); 1541 | // Add a newline for spacing before the progress bar if the filter line was shown 1542 | console.log(); 1543 | } 1544 | // --- Conditional Messages for No Subtasks Shown --- 1545 | } else if (statusFilter && originalSubtaskCount === 0) { 1546 | // Case where filter applied, but the parent task had 0 subtasks originally 1547 | console.log( 1548 | boxen( 1549 | chalk.yellow( 1550 | `No subtasks found matching status: ${statusFilter} (Task has no subtasks)` 1551 | ), 1552 | { 1553 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1554 | margin: { top: 1, bottom: 0 }, 1555 | borderColor: 'yellow', 1556 | borderStyle: 'round' 1557 | } 1558 | ) 1559 | ); 1560 | } else if ( 1561 | statusFilter && 1562 | originalSubtaskCount > 0 && 1563 | task.subtasks.length === 0 1564 | ) { 1565 | // Case where filter applied, original subtasks existed, but none matched 1566 | console.log( 1567 | boxen( 1568 | chalk.yellow( 1569 | `No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)` 1570 | ), 1571 | { 1572 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1573 | margin: { top: 1, bottom: 0 }, 1574 | borderColor: 'yellow', 1575 | borderStyle: 'round' 1576 | } 1577 | ) 1578 | ); 1579 | } else if ( 1580 | !statusFilter && 1581 | (!originalSubtasks || originalSubtasks.length === 0) 1582 | ) { 1583 | // Case where NO filter applied AND the task genuinely has no subtasks 1584 | // Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks 1585 | const actualSubtasks = originalSubtasks || task.subtasks; 1586 | if (!actualSubtasks || actualSubtasks.length === 0) { 1587 | console.log( 1588 | boxen( 1589 | chalk.yellow('No subtasks found. Consider breaking down this task:') + 1590 | '\n' + 1591 | chalk.white( 1592 | `Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}` 1593 | ), 1594 | { 1595 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1596 | borderColor: 'yellow', 1597 | borderStyle: 'round', 1598 | margin: { top: 1, bottom: 0 } 1599 | } 1600 | ) 1601 | ); 1602 | } 1603 | } 1604 | 1605 | // --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) --- 1606 | // Determine the list to use for progress calculation (always the original if available and filtering happened) 1607 | const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks 1608 | 1609 | // Only show progress if there are actually subtasks 1610 | if (subtasksForProgress && subtasksForProgress.length > 0) { 1611 | const totalSubtasks = subtasksForProgress.length; 1612 | const completedSubtasks = subtasksForProgress.filter( 1613 | (st) => st.status === 'done' || st.status === 'completed' 1614 | ).length; 1615 | 1616 | // Count other statuses from the original/complete list 1617 | const inProgressSubtasks = subtasksForProgress.filter( 1618 | (st) => st.status === 'in-progress' 1619 | ).length; 1620 | const pendingSubtasks = subtasksForProgress.filter( 1621 | (st) => st.status === 'pending' 1622 | ).length; 1623 | const blockedSubtasks = subtasksForProgress.filter( 1624 | (st) => st.status === 'blocked' 1625 | ).length; 1626 | const deferredSubtasks = subtasksForProgress.filter( 1627 | (st) => st.status === 'deferred' 1628 | ).length; 1629 | const cancelledSubtasks = subtasksForProgress.filter( 1630 | (st) => st.status === 'cancelled' 1631 | ).length; 1632 | 1633 | const statusBreakdown = { 1634 | // Calculate breakdown based on the complete list 1635 | 'in-progress': (inProgressSubtasks / totalSubtasks) * 100, 1636 | pending: (pendingSubtasks / totalSubtasks) * 100, 1637 | blocked: (blockedSubtasks / totalSubtasks) * 100, 1638 | deferred: (deferredSubtasks / totalSubtasks) * 100, 1639 | cancelled: (cancelledSubtasks / totalSubtasks) * 100 1640 | }; 1641 | const completionPercentage = (completedSubtasks / totalSubtasks) * 100; 1642 | 1643 | const availableWidth = process.stdout.columns || 80; 1644 | const boxPadding = 2; 1645 | const boxBorders = 2; 1646 | const percentTextLength = 5; 1647 | const progressBarLength = Math.max( 1648 | 20, 1649 | Math.min( 1650 | 60, 1651 | availableWidth - boxPadding - boxBorders - percentTextLength - 35 1652 | ) 1653 | ); 1654 | 1655 | const statusCounts = 1656 | `${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` + 1657 | `${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`; 1658 | 1659 | console.log( 1660 | boxen( 1661 | chalk.white.bold('Subtask Progress:') + 1662 | '\n\n' + 1663 | `${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + 1664 | `${statusCounts}\n` + 1665 | `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, 1666 | { 1667 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1668 | borderColor: 'blue', 1669 | borderStyle: 'round', 1670 | margin: { top: 1, bottom: 0 }, 1671 | width: Math.min(availableWidth - 10, 100), 1672 | textAlignment: 'left' 1673 | } 1674 | ) 1675 | ); 1676 | } 1677 | 1678 | // --- Suggested Actions --- 1679 | const actions = []; 1680 | let actionNumber = 1; 1681 | 1682 | // Basic actions 1683 | actions.push( 1684 | `${chalk.cyan(`${actionNumber}.`)} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}` 1685 | ); 1686 | actionNumber++; 1687 | actions.push( 1688 | `${chalk.cyan(`${actionNumber}.`)} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}` 1689 | ); 1690 | actionNumber++; 1691 | 1692 | // Subtask-related action 1693 | if (subtasksForProgress && subtasksForProgress.length > 0) { 1694 | actions.push( 1695 | `${chalk.cyan(`${actionNumber}.`)} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` 1696 | ); 1697 | } else { 1698 | actions.push( 1699 | `${chalk.cyan(`${actionNumber}.`)} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}` 1700 | ); 1701 | } 1702 | actionNumber++; 1703 | 1704 | // Complexity-based scope adjustment actions 1705 | if (task.complexityScore) { 1706 | const complexityScore = task.complexityScore; 1707 | actions.push( 1708 | `${chalk.cyan(`${actionNumber}.`)} Re-analyze complexity: ${chalk.yellow(`task-master analyze-complexity --id=${task.id}`)}` 1709 | ); 1710 | actionNumber++; 1711 | 1712 | // Add scope adjustment suggestions based on current complexity 1713 | if (complexityScore >= 7) { 1714 | // High complexity - suggest scoping down 1715 | actions.push( 1716 | `${chalk.cyan(`${actionNumber}.`)} Scope down (simplify): ${chalk.yellow(`task-master scope-down --id=${task.id} --strength=regular`)}` 1717 | ); 1718 | actionNumber++; 1719 | if (complexityScore >= 9) { 1720 | actions.push( 1721 | `${chalk.cyan(`${actionNumber}.`)} Heavy scope down: ${chalk.yellow(`task-master scope-down --id=${task.id} --strength=heavy`)}` 1722 | ); 1723 | actionNumber++; 1724 | } 1725 | } else if (complexityScore <= 4) { 1726 | // Low complexity - suggest scoping up 1727 | actions.push( 1728 | `${chalk.cyan(`${actionNumber}.`)} Scope up (add detail): ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=regular`)}` 1729 | ); 1730 | actionNumber++; 1731 | if (complexityScore <= 2) { 1732 | actions.push( 1733 | `${chalk.cyan(`${actionNumber}.`)} Heavy scope up: ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=heavy`)}` 1734 | ); 1735 | actionNumber++; 1736 | } 1737 | } else { 1738 | // Medium complexity (5-6) - offer both options 1739 | actions.push( 1740 | `${chalk.cyan(`${actionNumber}.`)} Scope up/down: ${chalk.yellow(`task-master scope-up --id=${task.id} --strength=light`)} or ${chalk.yellow(`scope-down --id=${task.id} --strength=light`)}` 1741 | ); 1742 | actionNumber++; 1743 | } 1744 | } 1745 | 1746 | console.log( 1747 | boxen(chalk.white.bold('Suggested Actions:') + '\n' + actions.join('\n'), { 1748 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 1749 | borderColor: 'green', 1750 | borderStyle: 'round', 1751 | margin: { top: 1 } 1752 | }) 1753 | ); 1754 | 1755 | // Show FYI notice if migration occurred 1756 | displayTaggedTasksFYI(data); 1757 | } 1758 | 1759 | /** 1760 | * Display the complexity analysis report in a nice format 1761 | * @param {string} reportPath - Path to the complexity report file 1762 | */ 1763 | async function displayComplexityReport(reportPath) { 1764 | // Check if the report exists 1765 | if (!fs.existsSync(reportPath)) { 1766 | console.log( 1767 | boxen( 1768 | chalk.yellow(`No complexity report found at ${reportPath}\n\n`) + 1769 | 'Would you like to generate one now?', 1770 | { 1771 | padding: 1, 1772 | borderColor: 'yellow', 1773 | borderStyle: 'round', 1774 | margin: { top: 1 } 1775 | } 1776 | ) 1777 | ); 1778 | 1779 | const rl = readline.createInterface({ 1780 | input: process.stdin, 1781 | output: process.stdout 1782 | }); 1783 | 1784 | const answer = await new Promise((resolve) => { 1785 | rl.question(chalk.cyan('Generate complexity report? (y/n): '), resolve); 1786 | }); 1787 | rl.close(); 1788 | 1789 | if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { 1790 | // Call the analyze-complexity command 1791 | console.log(chalk.blue('Generating complexity report...')); 1792 | const tasksPath = TASKMASTER_TASKS_FILE; 1793 | if (!fs.existsSync(tasksPath)) { 1794 | console.error( 1795 | '❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file.' 1796 | ); 1797 | return null; 1798 | } 1799 | 1800 | await analyzeTaskComplexity({ 1801 | output: reportPath, 1802 | research: false, // Default to no research for speed 1803 | file: tasksPath 1804 | }); 1805 | // Read the newly generated report 1806 | return displayComplexityReport(reportPath); 1807 | } else { 1808 | console.log(chalk.yellow('Report generation cancelled.')); 1809 | return; 1810 | } 1811 | } 1812 | 1813 | // Read the report 1814 | let report; 1815 | try { 1816 | report = JSON.parse(fs.readFileSync(reportPath, 'utf8')); 1817 | } catch (error) { 1818 | log('error', `Error reading complexity report: ${error.message}`); 1819 | return; 1820 | } 1821 | 1822 | // Display report header 1823 | console.log( 1824 | boxen(chalk.white.bold('Task Complexity Analysis Report'), { 1825 | padding: 1, 1826 | borderColor: 'blue', 1827 | borderStyle: 'round', 1828 | margin: { top: 1, bottom: 1 } 1829 | }) 1830 | ); 1831 | 1832 | // Display metadata 1833 | const metaTable = new Table({ 1834 | style: { 1835 | head: [], 1836 | border: [], 1837 | 'padding-top': 0, 1838 | 'padding-bottom': 0, 1839 | compact: true 1840 | }, 1841 | chars: { 1842 | mid: '', 1843 | 'left-mid': '', 1844 | 'mid-mid': '', 1845 | 'right-mid': '' 1846 | }, 1847 | colWidths: [20, 50] 1848 | }); 1849 | 1850 | metaTable.push( 1851 | [ 1852 | chalk.cyan.bold('Generated:'), 1853 | new Date(report.meta.generatedAt).toLocaleString() 1854 | ], 1855 | [chalk.cyan.bold('Tasks Analyzed:'), report.meta.tasksAnalyzed], 1856 | [chalk.cyan.bold('Threshold Score:'), report.meta.thresholdScore], 1857 | [chalk.cyan.bold('Project:'), report.meta.projectName], 1858 | [ 1859 | chalk.cyan.bold('Research-backed:'), 1860 | report.meta.usedResearch ? 'Yes' : 'No' 1861 | ] 1862 | ); 1863 | 1864 | console.log(metaTable.toString()); 1865 | 1866 | // Sort tasks by complexity score (highest first) 1867 | const sortedTasks = [...report.complexityAnalysis].sort( 1868 | (a, b) => b.complexityScore - a.complexityScore 1869 | ); 1870 | 1871 | // Determine which tasks need expansion based on threshold 1872 | const tasksNeedingExpansion = sortedTasks.filter( 1873 | (task) => task.complexityScore >= report.meta.thresholdScore 1874 | ); 1875 | const simpleTasks = sortedTasks.filter( 1876 | (task) => task.complexityScore < report.meta.thresholdScore 1877 | ); 1878 | 1879 | // Create progress bar to show complexity distribution 1880 | const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10) 1881 | sortedTasks.forEach((task) => { 1882 | if (task.complexityScore < 5) complexityDistribution[0]++; 1883 | else if (task.complexityScore < 8) complexityDistribution[1]++; 1884 | else complexityDistribution[2]++; 1885 | }); 1886 | 1887 | const percentLow = Math.round( 1888 | (complexityDistribution[0] / sortedTasks.length) * 100 1889 | ); 1890 | const percentMedium = Math.round( 1891 | (complexityDistribution[1] / sortedTasks.length) * 100 1892 | ); 1893 | const percentHigh = Math.round( 1894 | (complexityDistribution[2] / sortedTasks.length) * 100 1895 | ); 1896 | 1897 | console.log( 1898 | boxen( 1899 | chalk.white.bold('Complexity Distribution\n\n') + 1900 | `${chalk.green.bold('Low (1-4):')} ${complexityDistribution[0]} tasks (${percentLow}%)\n` + 1901 | `${chalk.yellow.bold('Medium (5-7):')} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` + 1902 | `${chalk.red.bold('High (8-10):')} ${complexityDistribution[2]} tasks (${percentHigh}%)`, 1903 | { 1904 | padding: 1, 1905 | borderColor: 'cyan', 1906 | borderStyle: 'round', 1907 | margin: { top: 1, bottom: 1 } 1908 | } 1909 | ) 1910 | ); 1911 | 1912 | // Get terminal width 1913 | const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect 1914 | 1915 | // Calculate dynamic column widths 1916 | const idWidth = 12; 1917 | const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width 1918 | const scoreWidth = 8; 1919 | const subtasksWidth = 8; 1920 | // Command column gets the remaining space (minus some buffer for borders) 1921 | const commandWidth = 1922 | terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10; 1923 | 1924 | // Create table with new column widths and word wrapping 1925 | const complexTable = new Table({ 1926 | head: [ 1927 | chalk.yellow.bold('ID'), 1928 | chalk.yellow.bold('Title'), 1929 | chalk.yellow.bold('Score'), 1930 | chalk.yellow.bold('Subtasks'), 1931 | chalk.yellow.bold('Expansion Command') 1932 | ], 1933 | colWidths: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth], 1934 | style: { head: [], border: [] }, 1935 | wordWrap: true, 1936 | wrapOnWordBoundary: true 1937 | }); 1938 | 1939 | // When adding rows, don't truncate the expansion command 1940 | tasksNeedingExpansion.forEach((task) => { 1941 | const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ''}`; 1942 | 1943 | complexTable.push([ 1944 | task.taskId, 1945 | truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability 1946 | getComplexityWithColor(task.complexityScore), 1947 | task.recommendedSubtasks, 1948 | chalk.cyan(expansionCommand) // Don't truncate - allow wrapping 1949 | ]); 1950 | }); 1951 | 1952 | console.log(complexTable.toString()); 1953 | 1954 | // Create table for simple tasks 1955 | if (simpleTasks.length > 0) { 1956 | console.log( 1957 | boxen(chalk.green.bold(`Simple Tasks (${simpleTasks.length})`), { 1958 | padding: { left: 2, right: 2, top: 0, bottom: 0 }, 1959 | margin: { top: 1, bottom: 0 }, 1960 | borderColor: 'green', 1961 | borderStyle: 'round' 1962 | }) 1963 | ); 1964 | 1965 | const simpleTable = new Table({ 1966 | head: [ 1967 | chalk.green.bold('ID'), 1968 | chalk.green.bold('Title'), 1969 | chalk.green.bold('Score'), 1970 | chalk.green.bold('Reasoning') 1971 | ], 1972 | colWidths: [5, 40, 8, 50], 1973 | style: { head: [], border: [] } 1974 | }); 1975 | 1976 | simpleTasks.forEach((task) => { 1977 | simpleTable.push([ 1978 | task.taskId, 1979 | truncate(task.taskTitle, 37), 1980 | getComplexityWithColor(task.complexityScore), 1981 | truncate(task.reasoning, 47) 1982 | ]); 1983 | }); 1984 | 1985 | console.log(simpleTable.toString()); 1986 | } 1987 | 1988 | // Show action suggestions 1989 | console.log( 1990 | boxen( 1991 | chalk.white.bold('Suggested Actions:') + 1992 | '\n\n' + 1993 | `${chalk.cyan('1.')} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` + 1994 | `${chalk.cyan('2.')} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` + 1995 | `${chalk.cyan('3.')} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`, 1996 | { 1997 | padding: 1, 1998 | borderColor: 'cyan', 1999 | borderStyle: 'round', 2000 | margin: { top: 1 } 2001 | } 2002 | ) 2003 | ); 2004 | } 2005 | 2006 | /** 2007 | * Generate a prompt for complexity analysis 2008 | * @param {Object} tasksData - Tasks data object containing tasks array 2009 | * @returns {string} Generated prompt 2010 | */ 2011 | function generateComplexityAnalysisPrompt(tasksData) { 2012 | const defaultSubtasks = getDefaultSubtasks(null); // Use the getter 2013 | return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: 2014 | 2015 | ${tasksData.tasks 2016 | .map( 2017 | (task) => ` 2018 | Task ID: ${task.id} 2019 | Title: ${task.title} 2020 | Description: ${task.description} 2021 | Details: ${task.details} 2022 | Dependencies: ${JSON.stringify(task.dependencies || [])} 2023 | Priority: ${task.priority || 'medium'} 2024 | ` 2025 | ) 2026 | .join('\n---\n')} 2027 | 2028 | Analyze each task and return a JSON array with the following structure for each task: 2029 | [ 2030 | { 2031 | "taskId": number, 2032 | "taskTitle": string, 2033 | "complexityScore": number (1-10), 2034 | "recommendedSubtasks": number (${Math.max(3, defaultSubtasks - 1)}-${Math.min(8, defaultSubtasks + 2)}), 2035 | "expansionPrompt": string (a specific prompt for generating good subtasks), 2036 | "reasoning": string (brief explanation of your assessment) 2037 | }, 2038 | ... 2039 | ] 2040 | 2041 | IMPORTANT: Make sure to include an analysis for EVERY task listed above, with the correct taskId matching each task's ID. 2042 | `; 2043 | } 2044 | 2045 | /** 2046 | * Confirm overwriting existing tasks.json file 2047 | * @param {string} tasksPath - Path to the tasks.json file 2048 | * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise 2049 | */ 2050 | async function confirmTaskOverwrite(tasksPath) { 2051 | console.log( 2052 | boxen( 2053 | chalk.yellow( 2054 | "It looks like you've already generated tasks for this project.\n" 2055 | ) + 2056 | chalk.yellow( 2057 | 'Executing this command will overwrite any existing tasks.' 2058 | ), 2059 | { 2060 | padding: 1, 2061 | borderColor: 'yellow', 2062 | borderStyle: 'round', 2063 | margin: { top: 1 } 2064 | } 2065 | ) 2066 | ); 2067 | 2068 | const rl = readline.createInterface({ 2069 | input: process.stdin, 2070 | output: process.stdout 2071 | }); 2072 | 2073 | const answer = await new Promise((resolve) => { 2074 | rl.question( 2075 | chalk.cyan('Are you sure you wish to continue? (y/N): '), 2076 | resolve 2077 | ); 2078 | }); 2079 | rl.close(); 2080 | 2081 | return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; 2082 | } 2083 | 2084 | /** 2085 | * Displays the API key status for different providers. 2086 | * @param {Array<{provider: string, cli: boolean, mcp: boolean}>} statusReport - The report generated by getApiKeyStatusReport. 2087 | */ 2088 | function displayApiKeyStatus(statusReport) { 2089 | if (!statusReport || statusReport.length === 0) { 2090 | console.log(chalk.yellow('No API key status information available.')); 2091 | return; 2092 | } 2093 | 2094 | const table = new Table({ 2095 | head: [ 2096 | chalk.cyan('Provider'), 2097 | chalk.cyan('CLI Key (.env)'), 2098 | chalk.cyan('MCP Key (mcp.json)') 2099 | ], 2100 | colWidths: [15, 20, 25], 2101 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' } 2102 | }); 2103 | 2104 | statusReport.forEach(({ provider, cli, mcp }) => { 2105 | const cliStatus = cli ? chalk.green('✅ Found') : chalk.red('❌ Missing'); 2106 | const mcpStatus = mcp ? chalk.green('✅ Found') : chalk.red('❌ Missing'); 2107 | // Capitalize provider name for display 2108 | const providerName = provider.charAt(0).toUpperCase() + provider.slice(1); 2109 | table.push([providerName, cliStatus, mcpStatus]); 2110 | }); 2111 | 2112 | console.log(chalk.bold('\n🔑 API Key Status:')); 2113 | console.log(table.toString()); 2114 | console.log( 2115 | chalk.gray( 2116 | ` Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in ${TASKMASTER_CONFIG_FILE}.` 2117 | ) 2118 | ); 2119 | } 2120 | 2121 | // --- Formatting Helpers (Potentially move some to utils.js if reusable) --- 2122 | 2123 | const formatSweScoreWithTertileStars = (score, allModels) => { 2124 | // ... (Implementation from previous version or refine) ... 2125 | if (score === null || score === undefined || score <= 0) return 'N/A'; 2126 | const formattedPercentage = `${(score * 100).toFixed(1)}%`; 2127 | 2128 | const validScores = allModels 2129 | .map((m) => m.sweScore) 2130 | .filter((s) => s !== null && s !== undefined && s > 0); 2131 | const sortedScores = [...validScores].sort((a, b) => b - a); 2132 | const n = sortedScores.length; 2133 | let stars = chalk.gray('☆☆☆'); 2134 | 2135 | if (n > 0) { 2136 | const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); 2137 | const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); 2138 | if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow('★★★'); 2139 | else if (score >= sortedScores[midThirdIndex]) 2140 | stars = chalk.yellow('★★') + chalk.gray('☆'); 2141 | else stars = chalk.yellow('★') + chalk.gray('☆☆'); 2142 | } 2143 | return `${formattedPercentage} ${stars}`; 2144 | }; 2145 | 2146 | const formatCost = (costObj) => { 2147 | // ... (Implementation from previous version or refine) ... 2148 | if (!costObj) return 'N/A'; 2149 | if (costObj.input === 0 && costObj.output === 0) { 2150 | return chalk.green('Free'); 2151 | } 2152 | const formatSingleCost = (costValue) => { 2153 | if (costValue === null || costValue === undefined) return 'N/A'; 2154 | const isInteger = Number.isInteger(costValue); 2155 | return `$${costValue.toFixed(isInteger ? 0 : 2)}`; 2156 | }; 2157 | return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`; 2158 | }; 2159 | 2160 | // --- Display Functions --- 2161 | 2162 | /** 2163 | * Displays the currently configured active models. 2164 | * @param {ConfigData} configData - The active configuration data. 2165 | * @param {AvailableModel[]} allAvailableModels - Needed for SWE score tertiles. 2166 | */ 2167 | function displayModelConfiguration(configData, allAvailableModels = []) { 2168 | console.log(chalk.cyan.bold('\nActive Model Configuration:')); 2169 | const active = configData.activeModels; 2170 | const activeTable = new Table({ 2171 | head: [ 2172 | 'Role', 2173 | 'Provider', 2174 | 'Model ID', 2175 | 'SWE Score', 2176 | 'Cost ($/1M tkns)' 2177 | // 'API Key Status' // Removed, handled by separate displayApiKeyStatus 2178 | ].map((h) => chalk.cyan.bold(h)), 2179 | colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths 2180 | style: { head: ['cyan', 'bold'] } 2181 | }); 2182 | 2183 | activeTable.push([ 2184 | chalk.white('Main'), 2185 | active.main.provider, 2186 | active.main.modelId, 2187 | formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels), 2188 | formatCost(active.main.cost) 2189 | // getCombinedStatus(active.main.keyStatus) // Removed 2190 | ]); 2191 | activeTable.push([ 2192 | chalk.white('Research'), 2193 | active.research.provider, 2194 | active.research.modelId, 2195 | formatSweScoreWithTertileStars( 2196 | active.research.sweScore, 2197 | allAvailableModels 2198 | ), 2199 | formatCost(active.research.cost) 2200 | // getCombinedStatus(active.research.keyStatus) // Removed 2201 | ]); 2202 | if (active.fallback && active.fallback.provider && active.fallback.modelId) { 2203 | activeTable.push([ 2204 | chalk.white('Fallback'), 2205 | active.fallback.provider, 2206 | active.fallback.modelId, 2207 | formatSweScoreWithTertileStars( 2208 | active.fallback.sweScore, 2209 | allAvailableModels 2210 | ), 2211 | formatCost(active.fallback.cost) 2212 | // getCombinedStatus(active.fallback.keyStatus) // Removed 2213 | ]); 2214 | } else { 2215 | activeTable.push([ 2216 | chalk.white('Fallback'), 2217 | chalk.gray('-'), 2218 | chalk.gray('(Not Set)'), 2219 | chalk.gray('-'), 2220 | chalk.gray('-') 2221 | // chalk.gray('-') // Removed 2222 | ]); 2223 | } 2224 | console.log(activeTable.toString()); 2225 | } 2226 | 2227 | /** 2228 | * Displays the list of available models not currently configured. 2229 | * @param {AvailableModel[]} availableModels - List of available models. 2230 | */ 2231 | function displayAvailableModels(availableModels) { 2232 | if (!availableModels || availableModels.length === 0) { 2233 | console.log( 2234 | chalk.gray('\n(No other models available or all are configured)') 2235 | ); 2236 | return; 2237 | } 2238 | 2239 | console.log(chalk.cyan.bold('\nOther Available Models:')); 2240 | const availableTable = new Table({ 2241 | head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map((h) => 2242 | chalk.cyan.bold(h) 2243 | ), 2244 | colWidths: [15, 40, 18, 25], 2245 | style: { head: ['cyan', 'bold'] } 2246 | }); 2247 | 2248 | availableModels.forEach((model) => { 2249 | availableTable.push([ 2250 | model.provider, 2251 | model.modelId, 2252 | formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison 2253 | formatCost(model.cost) 2254 | ]); 2255 | }); 2256 | console.log(availableTable.toString()); 2257 | 2258 | // --- Suggested Actions Section (moved here from models command) --- 2259 | console.log( 2260 | boxen( 2261 | chalk.white.bold('Next Steps:') + 2262 | '\n' + 2263 | chalk.cyan( 2264 | `1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}` 2265 | ) + 2266 | '\n' + 2267 | chalk.cyan( 2268 | `2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}` 2269 | ) + 2270 | '\n' + 2271 | chalk.cyan( 2272 | `3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}` 2273 | ) + 2274 | '\n' + 2275 | chalk.cyan( 2276 | `4. Run interactive setup: ${chalk.yellow('task-master models --setup')}` 2277 | ) + 2278 | '\n' + 2279 | chalk.cyan( 2280 | `5. Use custom ollama/openrouter models: ${chalk.yellow('task-master models --openrouter|ollama --set-main|research|fallback <model_id>')}` 2281 | ), 2282 | { 2283 | padding: 1, 2284 | borderColor: 'yellow', 2285 | borderStyle: 'round', 2286 | margin: { top: 1 } 2287 | } 2288 | ) 2289 | ); 2290 | } 2291 | 2292 | /** 2293 | * Displays AI usage telemetry summary in the CLI. 2294 | * @param {object} telemetryData - The telemetry data object. 2295 | * @param {string} outputType - 'cli' or 'mcp' (though typically only called for 'cli'). 2296 | */ 2297 | function displayAiUsageSummary(telemetryData, outputType = 'cli') { 2298 | if ( 2299 | (outputType !== 'cli' && outputType !== 'text') || 2300 | !telemetryData || 2301 | isSilentMode() 2302 | ) { 2303 | return; // Only display for CLI and if data exists and not in silent mode 2304 | } 2305 | 2306 | const { 2307 | modelUsed, 2308 | providerName, 2309 | inputTokens, 2310 | outputTokens, 2311 | totalTokens, 2312 | totalCost, 2313 | commandName 2314 | } = telemetryData; 2315 | 2316 | let summary = chalk.bold.blue('AI Usage Summary:') + '\n'; 2317 | summary += chalk.gray(` Command: ${commandName}\n`); 2318 | summary += chalk.gray(` Provider: ${providerName}\n`); 2319 | summary += chalk.gray(` Model: ${modelUsed}\n`); 2320 | summary += chalk.gray( 2321 | ` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n` 2322 | ); 2323 | summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`); 2324 | 2325 | console.log( 2326 | boxen(summary, { 2327 | padding: 1, 2328 | margin: { top: 1 }, 2329 | borderColor: 'blue', 2330 | borderStyle: 'round', 2331 | title: '💡 Telemetry', 2332 | titleAlignment: 'center' 2333 | }) 2334 | ); 2335 | } 2336 | 2337 | /** 2338 | * Display multiple tasks in a compact summary format with interactive drill-down 2339 | * @param {string} tasksPath - Path to the tasks.json file 2340 | * @param {Array<string>} taskIds - Array of task IDs to display 2341 | * @param {string} complexityReportPath - Path to complexity report 2342 | * @param {string} statusFilter - Optional status filter for subtasks 2343 | * @param {Object} context - Context object containing projectRoot and tag 2344 | * @param {string} [context.projectRoot] - Project root path 2345 | * @param {string} [context.tag] - Tag for the task 2346 | */ 2347 | async function displayMultipleTasksSummary( 2348 | tasksPath, 2349 | taskIds, 2350 | complexityReportPath = null, 2351 | statusFilter = null, 2352 | context = {} 2353 | ) { 2354 | displayBanner(); 2355 | 2356 | // Extract projectRoot and tag from context 2357 | const projectRoot = context.projectRoot || null; 2358 | const tag = context.tag || null; 2359 | 2360 | // Read the tasks file with proper projectRoot for tag resolution 2361 | const data = readJSON(tasksPath, projectRoot, tag); 2362 | if (!data || !data.tasks) { 2363 | log('error', 'No valid tasks found.'); 2364 | process.exit(1); 2365 | } 2366 | 2367 | // Read complexity report once 2368 | const complexityReport = readComplexityReport(complexityReportPath); 2369 | 2370 | // Find all requested tasks 2371 | const foundTasks = []; 2372 | const notFoundIds = []; 2373 | 2374 | taskIds.forEach((id) => { 2375 | const { task } = findTaskById( 2376 | data.tasks, 2377 | id, 2378 | complexityReport, 2379 | statusFilter 2380 | ); 2381 | if (task) { 2382 | foundTasks.push(task); 2383 | } else { 2384 | notFoundIds.push(id); 2385 | } 2386 | }); 2387 | 2388 | // Show not found tasks 2389 | if (notFoundIds.length > 0) { 2390 | console.log( 2391 | boxen(chalk.yellow(`Tasks not found: ${notFoundIds.join(', ')}`), { 2392 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2393 | borderColor: 'yellow', 2394 | borderStyle: 'round', 2395 | margin: { top: 1, bottom: 1 } 2396 | }) 2397 | ); 2398 | } 2399 | 2400 | if (foundTasks.length === 0) { 2401 | console.log( 2402 | boxen(chalk.red('No valid tasks found to display'), { 2403 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2404 | borderColor: 'red', 2405 | borderStyle: 'round', 2406 | margin: { top: 1 } 2407 | }) 2408 | ); 2409 | return; 2410 | } 2411 | 2412 | // Display header 2413 | console.log( 2414 | boxen( 2415 | chalk.white.bold( 2416 | `Task Summary (${foundTasks.length} task${foundTasks.length === 1 ? '' : 's'})` 2417 | ), 2418 | { 2419 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2420 | borderColor: 'blue', 2421 | borderStyle: 'round', 2422 | margin: { top: 1, bottom: 0 } 2423 | } 2424 | ) 2425 | ); 2426 | 2427 | // Calculate terminal width for responsive layout 2428 | const terminalWidth = process.stdout.columns || 100; 2429 | const availableWidth = terminalWidth - 10; 2430 | 2431 | // Create compact summary table 2432 | const summaryTable = new Table({ 2433 | head: [ 2434 | chalk.cyan.bold('ID'), 2435 | chalk.cyan.bold('Title'), 2436 | chalk.cyan.bold('Status'), 2437 | chalk.cyan.bold('Priority'), 2438 | chalk.cyan.bold('Subtasks'), 2439 | chalk.cyan.bold('Progress') 2440 | ], 2441 | colWidths: [ 2442 | Math.floor(availableWidth * 0.08), // ID: 8% 2443 | Math.floor(availableWidth * 0.35), // Title: 35% 2444 | Math.floor(availableWidth * 0.12), // Status: 12% 2445 | Math.floor(availableWidth * 0.1), // Priority: 10% 2446 | Math.floor(availableWidth * 0.15), // Subtasks: 15% 2447 | Math.floor(availableWidth * 0.2) // Progress: 20% 2448 | ], 2449 | style: { 2450 | head: [], 2451 | border: [], 2452 | 'padding-top': 0, 2453 | 'padding-bottom': 0, 2454 | compact: true 2455 | }, 2456 | chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, 2457 | wordWrap: true 2458 | }); 2459 | 2460 | // Add each task to the summary table 2461 | foundTasks.forEach((task) => { 2462 | // Handle subtask case 2463 | if (task.isSubtask || task.parentTask) { 2464 | const parentId = task.parentTask ? task.parentTask.id : 'Unknown'; 2465 | summaryTable.push([ 2466 | `${parentId}.${task.id}`, 2467 | truncate(task.title, Math.floor(availableWidth * 0.35) - 3), 2468 | getStatusWithColor(task.status || 'pending', true), 2469 | chalk.gray('(subtask)'), 2470 | chalk.gray('N/A'), 2471 | chalk.gray('N/A') 2472 | ]); 2473 | return; 2474 | } 2475 | 2476 | // Handle regular task 2477 | const priorityColors = { 2478 | high: chalk.red.bold, 2479 | medium: chalk.yellow, 2480 | low: chalk.gray 2481 | }; 2482 | const priorityColor = 2483 | priorityColors[task.priority || 'medium'] || chalk.white; 2484 | 2485 | // Calculate subtask summary 2486 | let subtaskSummary = chalk.gray('None'); 2487 | let progressBar = chalk.gray('N/A'); 2488 | 2489 | if (task.subtasks && task.subtasks.length > 0) { 2490 | const total = task.subtasks.length; 2491 | const completed = task.subtasks.filter( 2492 | (st) => st.status === 'done' || st.status === 'completed' 2493 | ).length; 2494 | const inProgress = task.subtasks.filter( 2495 | (st) => st.status === 'in-progress' 2496 | ).length; 2497 | const pending = task.subtasks.filter( 2498 | (st) => st.status === 'pending' 2499 | ).length; 2500 | 2501 | // Compact subtask count with status indicators 2502 | subtaskSummary = `${chalk.green(completed)}/${total}`; 2503 | if (inProgress > 0) 2504 | subtaskSummary += ` ${chalk.hex('#FFA500')(`+${inProgress}`)}`; 2505 | if (pending > 0) subtaskSummary += ` ${chalk.yellow(`(${pending})`)}`; 2506 | 2507 | // Mini progress bar (shorter than usual) 2508 | const completionPercentage = (completed / total) * 100; 2509 | const barLength = 8; // Compact bar 2510 | const statusBreakdown = { 2511 | 'in-progress': (inProgress / total) * 100, 2512 | pending: (pending / total) * 100 2513 | }; 2514 | progressBar = createProgressBar( 2515 | completionPercentage, 2516 | barLength, 2517 | statusBreakdown 2518 | ); 2519 | } 2520 | 2521 | summaryTable.push([ 2522 | task.id.toString(), 2523 | truncate(task.title, Math.floor(availableWidth * 0.35) - 3), 2524 | getStatusWithColor(task.status || 'pending', true), 2525 | priorityColor(task.priority || 'medium'), 2526 | subtaskSummary, 2527 | progressBar 2528 | ]); 2529 | }); 2530 | 2531 | console.log(summaryTable.toString()); 2532 | 2533 | // Interactive drill-down prompt 2534 | if (foundTasks.length > 1) { 2535 | console.log( 2536 | boxen( 2537 | chalk.white.bold('Interactive Options:') + 2538 | '\n' + 2539 | chalk.cyan('• Press Enter to view available actions for all tasks') + 2540 | '\n' + 2541 | chalk.cyan( 2542 | '• Type a task ID (e.g., "3" or "3.2") to view that specific task' 2543 | ) + 2544 | '\n' + 2545 | chalk.cyan('• Type "q" to quit'), 2546 | { 2547 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2548 | borderColor: 'green', 2549 | borderStyle: 'round', 2550 | margin: { top: 1 } 2551 | } 2552 | ) 2553 | ); 2554 | 2555 | const rl = readline.createInterface({ 2556 | input: process.stdin, 2557 | output: process.stdout 2558 | }); 2559 | 2560 | const choice = await new Promise((resolve) => { 2561 | rl.question(chalk.cyan('Your choice: '), resolve); 2562 | }); 2563 | rl.close(); 2564 | 2565 | if (choice.toLowerCase() === 'q') { 2566 | return; 2567 | } else if (choice.trim() === '') { 2568 | // Show action menu for selected tasks 2569 | console.log( 2570 | boxen( 2571 | chalk.white.bold('Available Actions for Selected Tasks:') + 2572 | '\n' + 2573 | chalk.cyan('1.') + 2574 | ' Mark all as in-progress' + 2575 | '\n' + 2576 | chalk.cyan('2.') + 2577 | ' Mark all as done' + 2578 | '\n' + 2579 | chalk.cyan('3.') + 2580 | ' Show next available task' + 2581 | '\n' + 2582 | chalk.cyan('4.') + 2583 | ' Expand all tasks (generate subtasks)' + 2584 | '\n' + 2585 | chalk.cyan('5.') + 2586 | ' View dependency relationships' + 2587 | '\n' + 2588 | chalk.cyan('6.') + 2589 | ' Generate task files' + 2590 | '\n' + 2591 | chalk.gray('Or type a task ID to view details'), 2592 | { 2593 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2594 | borderColor: 'blue', 2595 | borderStyle: 'round', 2596 | margin: { top: 1 } 2597 | } 2598 | ) 2599 | ); 2600 | 2601 | const rl2 = readline.createInterface({ 2602 | input: process.stdin, 2603 | output: process.stdout 2604 | }); 2605 | 2606 | const actionChoice = await new Promise((resolve) => { 2607 | rl2.question(chalk.cyan('Choose action (1-6): '), resolve); 2608 | }); 2609 | rl2.close(); 2610 | 2611 | const taskIdList = foundTasks.map((t) => t.id).join(','); 2612 | 2613 | switch (actionChoice.trim()) { 2614 | case '1': 2615 | console.log( 2616 | chalk.blue( 2617 | `\n→ Command: task-master set-status --id=${taskIdList} --status=in-progress` 2618 | ) 2619 | ); 2620 | console.log( 2621 | chalk.green( 2622 | '✓ Copy and run this command to mark all tasks as in-progress' 2623 | ) 2624 | ); 2625 | break; 2626 | case '2': 2627 | console.log( 2628 | chalk.blue( 2629 | `\n→ Command: task-master set-status --id=${taskIdList} --status=done` 2630 | ) 2631 | ); 2632 | console.log( 2633 | chalk.green('✓ Copy and run this command to mark all tasks as done') 2634 | ); 2635 | break; 2636 | case '3': 2637 | console.log(chalk.blue(`\n→ Command: task-master next`)); 2638 | console.log( 2639 | chalk.green( 2640 | '✓ Copy and run this command to see the next available task' 2641 | ) 2642 | ); 2643 | break; 2644 | case '4': 2645 | console.log( 2646 | chalk.blue( 2647 | `\n→ Command: task-master expand --id=${taskIdList} --research` 2648 | ) 2649 | ); 2650 | console.log( 2651 | chalk.green( 2652 | '✓ Copy and run this command to expand all selected tasks into subtasks' 2653 | ) 2654 | ); 2655 | break; 2656 | case '5': { 2657 | // Show dependency visualization 2658 | console.log(chalk.white.bold('\nDependency Relationships:')); 2659 | let hasDependencies = false; 2660 | foundTasks.forEach((task) => { 2661 | if (task.dependencies && task.dependencies.length > 0) { 2662 | console.log( 2663 | chalk.cyan( 2664 | `Task ${task.id} depends on: ${task.dependencies.join(', ')}` 2665 | ) 2666 | ); 2667 | hasDependencies = true; 2668 | } 2669 | }); 2670 | if (!hasDependencies) { 2671 | console.log(chalk.gray('No dependencies found for selected tasks')); 2672 | } 2673 | break; 2674 | } 2675 | case '6': 2676 | console.log(chalk.blue(`\n→ Command: task-master generate`)); 2677 | console.log( 2678 | chalk.green('✓ Copy and run this command to generate task files') 2679 | ); 2680 | break; 2681 | default: 2682 | if (actionChoice.trim().length > 0) { 2683 | console.log(chalk.yellow(`Invalid choice: ${actionChoice.trim()}`)); 2684 | console.log(chalk.gray('Please choose 1-6 or type a task ID')); 2685 | } 2686 | } 2687 | } else { 2688 | // Show specific task 2689 | await displayTaskById( 2690 | tasksPath, 2691 | choice.trim(), 2692 | complexityReportPath, 2693 | statusFilter, 2694 | context 2695 | ); 2696 | } 2697 | } else { 2698 | // Single task - show suggested actions 2699 | const task = foundTasks[0]; 2700 | console.log( 2701 | boxen( 2702 | chalk.white.bold('Suggested Actions:') + 2703 | '\n' + 2704 | `${chalk.cyan('1.')} View full details: ${chalk.yellow(`task-master show ${task.id}`)}\n` + 2705 | `${chalk.cyan('2.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` + 2706 | `${chalk.cyan('3.')} Mark as done: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}`, 2707 | { 2708 | padding: { top: 0, bottom: 0, left: 1, right: 1 }, 2709 | borderColor: 'green', 2710 | borderStyle: 'round', 2711 | margin: { top: 1 } 2712 | } 2713 | ) 2714 | ); 2715 | } 2716 | } 2717 | 2718 | /** 2719 | * Display context analysis results with beautiful formatting 2720 | * @param {Object} analysisData - Analysis data from ContextGatherer 2721 | * @param {string} semanticQuery - The original query used for semantic search 2722 | * @param {number} contextSize - Size of gathered context in characters 2723 | */ 2724 | function displayContextAnalysis(analysisData, semanticQuery, contextSize) { 2725 | if (isSilentMode() || !analysisData) return; 2726 | 2727 | const { highRelevance, mediumRelevance, recentTasks, allRelevantTasks } = 2728 | analysisData; 2729 | 2730 | // Create the context analysis display 2731 | let analysisContent = chalk.white.bold('Context Analysis') + '\n\n'; 2732 | 2733 | // Query info 2734 | analysisContent += 2735 | chalk.gray('Query: ') + chalk.white(`"${semanticQuery}"`) + '\n'; 2736 | analysisContent += 2737 | chalk.gray('Context size: ') + 2738 | chalk.cyan(`${contextSize.toLocaleString()} characters`) + 2739 | '\n'; 2740 | analysisContent += 2741 | chalk.gray('Tasks found: ') + 2742 | chalk.yellow(`${allRelevantTasks.length} relevant tasks`) + 2743 | '\n\n'; 2744 | 2745 | // High relevance matches 2746 | if (highRelevance.length > 0) { 2747 | analysisContent += chalk.green.bold('🎯 High Relevance Matches:') + '\n'; 2748 | highRelevance.slice(0, 3).forEach((task) => { 2749 | analysisContent += 2750 | chalk.green(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n'; 2751 | }); 2752 | if (highRelevance.length > 3) { 2753 | analysisContent += 2754 | chalk.green( 2755 | ` • ... and ${highRelevance.length - 3} more high relevance tasks` 2756 | ) + '\n'; 2757 | } 2758 | analysisContent += '\n'; 2759 | } 2760 | 2761 | // Medium relevance matches 2762 | if (mediumRelevance.length > 0) { 2763 | analysisContent += chalk.yellow.bold('📋 Medium Relevance Matches:') + '\n'; 2764 | mediumRelevance.slice(0, 3).forEach((task) => { 2765 | analysisContent += 2766 | chalk.yellow(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n'; 2767 | }); 2768 | if (mediumRelevance.length > 3) { 2769 | analysisContent += 2770 | chalk.yellow( 2771 | ` • ... and ${mediumRelevance.length - 3} more medium relevance tasks` 2772 | ) + '\n'; 2773 | } 2774 | analysisContent += '\n'; 2775 | } 2776 | 2777 | // Recent tasks (if they contributed) 2778 | const recentTasksNotInRelevance = recentTasks.filter( 2779 | (task) => 2780 | !highRelevance.some((hr) => hr.id === task.id) && 2781 | !mediumRelevance.some((mr) => mr.id === task.id) 2782 | ); 2783 | 2784 | if (recentTasksNotInRelevance.length > 0) { 2785 | analysisContent += chalk.cyan.bold('🕒 Recent Tasks (for context):') + '\n'; 2786 | recentTasksNotInRelevance.slice(0, 2).forEach((task) => { 2787 | analysisContent += 2788 | chalk.cyan(` • Task ${task.id}: ${truncate(task.title, 50)}`) + '\n'; 2789 | }); 2790 | if (recentTasksNotInRelevance.length > 2) { 2791 | analysisContent += 2792 | chalk.cyan( 2793 | ` • ... and ${recentTasksNotInRelevance.length - 2} more recent tasks` 2794 | ) + '\n'; 2795 | } 2796 | } 2797 | 2798 | console.log( 2799 | boxen(analysisContent, { 2800 | padding: { top: 1, bottom: 1, left: 2, right: 2 }, 2801 | margin: { top: 1, bottom: 0 }, 2802 | borderStyle: 'round', 2803 | borderColor: 'blue', 2804 | title: chalk.blue('🔍 Context Gathering'), 2805 | titleAlignment: 'center' 2806 | }) 2807 | ); 2808 | } 2809 | 2810 | // Export UI functions 2811 | export { 2812 | displayBanner, 2813 | displayTaggedTasksFYI, 2814 | startLoadingIndicator, 2815 | stopLoadingIndicator, 2816 | createProgressBar, 2817 | getStatusWithColor, 2818 | formatDependenciesWithStatus, 2819 | displayHelp, 2820 | getComplexityWithColor, 2821 | displayNextTask, 2822 | displayTaskById, 2823 | displayComplexityReport, 2824 | generateComplexityAnalysisPrompt, 2825 | confirmTaskOverwrite, 2826 | displayApiKeyStatus, 2827 | displayModelConfiguration, 2828 | displayAvailableModels, 2829 | displayAiUsageSummary, 2830 | displayMultipleTasksSummary, 2831 | succeedLoadingIndicator, 2832 | failLoadingIndicator, 2833 | warnLoadingIndicator, 2834 | infoLoadingIndicator, 2835 | displayContextAnalysis, 2836 | displayCurrentTagIndicator, 2837 | formatTaskIdForDisplay 2838 | }; 2839 | 2840 | /** 2841 | * Display enhanced error message for cross-tag dependency conflicts 2842 | * @param {Array} conflicts - Array of cross-tag dependency conflicts 2843 | * @param {string} sourceTag - Source tag name 2844 | * @param {string} targetTag - Target tag name 2845 | * @param {string} sourceIds - Source task IDs (comma-separated) 2846 | */ 2847 | export function displayCrossTagDependencyError( 2848 | conflicts, 2849 | sourceTag, 2850 | targetTag, 2851 | sourceIds 2852 | ) { 2853 | console.log( 2854 | chalk.red(`\n❌ Cannot move tasks from "${sourceTag}" to "${targetTag}"`) 2855 | ); 2856 | console.log(chalk.yellow(`\nCross-tag dependency conflicts detected:`)); 2857 | 2858 | if (conflicts.length > 0) { 2859 | conflicts.forEach((conflict) => { 2860 | console.log(` • ${conflict.message}`); 2861 | }); 2862 | } 2863 | 2864 | console.log(chalk.cyan(`\nResolution options:`)); 2865 | console.log( 2866 | ` 1. Move with dependencies: task-master move --from=${sourceIds} --from-tag=${sourceTag} --to-tag=${targetTag} --with-dependencies` 2867 | ); 2868 | console.log( 2869 | ` 2. Break dependencies: task-master move --from=${sourceIds} --from-tag=${sourceTag} --to-tag=${targetTag} --ignore-dependencies` 2870 | ); 2871 | console.log( 2872 | ` 3. Validate and fix dependencies: task-master validate-dependencies && task-master fix-dependencies` 2873 | ); 2874 | if (conflicts.length > 0) { 2875 | console.log( 2876 | ` 4. Move dependencies first: task-master move --from=${conflicts.map((c) => c.dependencyId).join(',')} --from-tag=${conflicts[0].dependencyTag} --to-tag=${targetTag}` 2877 | ); 2878 | } 2879 | } 2880 | 2881 | /** 2882 | * Helper function to format task ID for display, handling edge cases with explicit labels 2883 | * Builds on the existing formatTaskId utility but adds user-friendly display for edge cases 2884 | * @param {*} taskId - The task ID to format 2885 | * @returns {string} Formatted task ID for display 2886 | */ 2887 | function formatTaskIdForDisplay(taskId) { 2888 | if (taskId === null) return 'null'; 2889 | if (taskId === undefined) return 'undefined'; 2890 | if (taskId === '') return '(empty)'; 2891 | 2892 | // Use existing formatTaskId for normal cases, with fallback to 'unknown' 2893 | return formatTaskId(taskId) || 'unknown'; 2894 | } 2895 | 2896 | /** 2897 | * Display enhanced error message for subtask movement restriction 2898 | * @param {string} taskId - The subtask ID that cannot be moved 2899 | * @param {string} sourceTag - Source tag name 2900 | * @param {string} targetTag - Target tag name 2901 | */ 2902 | export function displaySubtaskMoveError(taskId, sourceTag, targetTag) { 2903 | // Handle null/undefined taskId but preserve the actual value for display 2904 | const displayTaskId = formatTaskIdForDisplay(taskId); 2905 | 2906 | // Safe taskId for operations that need a valid string 2907 | const safeTaskId = taskId || 'unknown'; 2908 | 2909 | // Validate taskId format before splitting 2910 | let parentId = safeTaskId; 2911 | if (safeTaskId.includes('.')) { 2912 | const parts = safeTaskId.split('.'); 2913 | // Check if it's a valid subtask format (parentId.subtaskId) 2914 | if (parts.length === 2 && parts[0] && parts[1]) { 2915 | parentId = parts[0]; 2916 | } else { 2917 | // Invalid format - log warning and use the original taskId 2918 | console.log( 2919 | chalk.yellow( 2920 | `\n⚠️ Warning: Unexpected taskId format "${safeTaskId}". Using as-is for command suggestions.` 2921 | ) 2922 | ); 2923 | parentId = safeTaskId; 2924 | } 2925 | } 2926 | 2927 | console.log( 2928 | chalk.red(`\n❌ Cannot move subtask ${displayTaskId} directly between tags`) 2929 | ); 2930 | console.log(chalk.yellow(`\nSubtask movement restriction:`)); 2931 | console.log(` • Subtasks cannot be moved directly between tags`); 2932 | console.log(` • They must be promoted to full tasks first`); 2933 | console.log(` • Source tag: "${sourceTag}"`); 2934 | console.log(` • Target tag: "${targetTag}"`); 2935 | 2936 | console.log(chalk.cyan(`\nResolution options:`)); 2937 | console.log( 2938 | ` 1. Promote subtask to full task: task-master remove-subtask --id=${displayTaskId} --convert` 2939 | ); 2940 | console.log( 2941 | ` 2. Then move the promoted task: task-master move --from=${parentId} --from-tag=${sourceTag} --to-tag=${targetTag}` 2942 | ); 2943 | console.log( 2944 | ` 3. Or move the parent task with all subtasks: task-master move --from=${parentId} --from-tag=${sourceTag} --to-tag=${targetTag} --with-dependencies` 2945 | ); 2946 | } 2947 | 2948 | /** 2949 | * Display enhanced error message for invalid tag combinations 2950 | * @param {string} sourceTag - Source tag name 2951 | * @param {string} targetTag - Target tag name 2952 | * @param {string} reason - Reason for the error 2953 | */ 2954 | export function displayInvalidTagCombinationError( 2955 | sourceTag, 2956 | targetTag, 2957 | reason 2958 | ) { 2959 | console.log(chalk.red(`\n❌ Invalid tag combination`)); 2960 | console.log(chalk.yellow(`\nError details:`)); 2961 | console.log(` • Source tag: "${sourceTag}"`); 2962 | console.log(` • Target tag: "${targetTag}"`); 2963 | console.log(` • Reason: ${reason}`); 2964 | 2965 | console.log(chalk.cyan(`\nResolution options:`)); 2966 | console.log(` 1. Use different tags for cross-tag moves`); 2967 | console.log( 2968 | ` 2. Use within-tag move: task-master move --from=<id> --to=<id> --tag=${sourceTag}` 2969 | ); 2970 | console.log(` 3. Check available tags: task-master tags`); 2971 | } 2972 | 2973 | /** 2974 | * Display helpful hints for dependency validation commands 2975 | * @param {string} context - Context for the hints (e.g., 'before-move', 'after-error') 2976 | */ 2977 | export function displayDependencyValidationHints(context = 'general') { 2978 | const hints = { 2979 | 'before-move': [ 2980 | '💡 Tip: Run "task-master validate-dependencies" to check for dependency issues before moving tasks', 2981 | '💡 Tip: Use "task-master fix-dependencies" to automatically resolve common dependency problems', 2982 | '💡 Tip: Consider using --with-dependencies flag to move dependent tasks together' 2983 | ], 2984 | 'after-error': [ 2985 | '🔧 Quick fix: Run "task-master validate-dependencies" to identify specific issues', 2986 | '🔧 Quick fix: Use "task-master fix-dependencies" to automatically resolve problems', 2987 | '🔧 Quick fix: Check "task-master show <id>" to see task dependencies before moving' 2988 | ], 2989 | general: [ 2990 | '💡 Use "task-master validate-dependencies" to check for dependency issues', 2991 | '💡 Use "task-master fix-dependencies" to automatically resolve problems', 2992 | '💡 Use "task-master show <id>" to view task dependencies', 2993 | '💡 Use --with-dependencies flag to move dependent tasks together' 2994 | ] 2995 | }; 2996 | 2997 | const relevantHints = hints[context] || hints.general; 2998 | 2999 | console.log(chalk.cyan(`\nHelpful hints:`)); 3000 | // Convert to Set to ensure only unique hints are displayed 3001 | const uniqueHints = new Set(relevantHints); 3002 | uniqueHints.forEach((hint) => { 3003 | console.log(` ${hint}`); 3004 | }); 3005 | } 3006 | ```