Compare commits

...

4 Commits

Author SHA1 Message Date
Ralph Khreish
83c0eec982 feat: experimental, bundle our commands into bins in order to be able to remove --package 2025-06-20 19:45:57 +03:00
Joe Danziger
a2a3229fd0 feat: Enhanced project initialization with Git worktree detection (#743)
* Fix Cursor deeplink installation with copy-paste instructions (#723)

* detect git worktree

* add changeset

* add aliases and git flags

* add changeset

* rename and update test

* add store tasks in git functionality

* update changeset

* fix newline

* remove unused import

* update command wording

* update command option text
2025-06-20 17:58:50 +02:00
Joe Danziger
b592dff8bc Rename Roo Code "Boomerang" role to "Orchestrator" (#831) 2025-06-20 17:20:14 +03:00
Ralph Khreish
e9d1bc2385 Feature/compatibleapisupport (#830)
* add compatible platform api support

* Adjust the code according to the suggestions

* Fully revised as requested: restored all required checks, improved compatibility, and converted all comments to English.

* feat: Add support for compatible API endpoints via baseURL

* chore: Add changeset for compatible API support

* chore: cleanup

* chore: improve changeset

* fix: package-lock.json

* fix: package-lock.json

---------

Co-authored-by: He-Xun <1226807142@qq.com>
2025-06-20 16:18:03 +02:00
27 changed files with 3980 additions and 220 deletions

View File

@@ -0,0 +1,8 @@
---
"task-master-ai": minor
---
Can now configure baseURL of provider with `<PROVIDER>_BASE_URL`
- For example:
- `OPENAI_BASE_URL`

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Rename Roo Code Boomerang role to Orchestrator

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Improve mcp keys check in cursor

View File

@@ -0,0 +1,22 @@
---
"task-master-ai": minor
---
- **Git Worktree Detection:**
- Now properly skips Git initialization when inside existing Git worktree
- Prevents accidental nested repository creation
- **Flag System Overhaul:**
- `--git`/`--no-git` controls repository initialization
- `--aliases`/`--no-aliases` consistently manages shell alias creation
- `--git-tasks`/`--no-git-tasks` controls whether task files are stored in Git
- `--dry-run` accurately previews all initialization behaviors
- **GitTasks Functionality:**
- New `--git-tasks` flag includes task files in Git (comments them out in .gitignore)
- New `--no-git-tasks` flag excludes task files from Git (default behavior)
- Supports both CLI and MCP interfaces with proper parameter passing
**Implementation Details:**
- Added explicit Git worktree detection before initialization
- Refactored flag processing to ensure consistent behavior
- Fixes #734

View File

@@ -26,6 +26,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
* `--description <text>`: `Provide a brief description for your project.`
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
* `--no-git`: `Skip initializing a Git repository entirely.`
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
* **Usage:** Run this once at the beginning of a new project.
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
@@ -36,6 +37,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
* `authorName`: `Author name.` (CLI: `--author <author>`)
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
* `noGit`: `Skip initializing a Git repository entirely. Default is false.` (CLI: `--no-git`)
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.

View File

@@ -9,32 +9,32 @@
**Architectural Design & Planning Role (Delegated Tasks):**
Your primary role when activated via `new_task` by the Boomerang orchestrator is to perform specific architectural, design, or planning tasks, focusing on the instructions provided in the delegation message and referencing the relevant `taskmaster-ai` task ID.
Your primary role when activated via `new_task` by the Orchestrator is to perform specific architectural, design, or planning tasks, focusing on the instructions provided in the delegation message and referencing the relevant `taskmaster-ai` task ID.
1. **Analyze Delegated Task:** Carefully examine the `message` provided by Boomerang. This message contains the specific task scope, context (including the `taskmaster-ai` task ID), and constraints.
1. **Analyze Delegated Task:** Carefully examine the `message` provided by Orchestrator. This message contains the specific task scope, context (including the `taskmaster-ai` task ID), and constraints.
2. **Information Gathering (As Needed):** Use analysis tools to fulfill the task:
* `list_files`: Understand project structure.
* `read_file`: Examine specific code, configuration, or documentation files relevant to the architectural task.
* `list_code_definition_names`: Analyze code structure and relationships.
* `use_mcp_tool` (taskmaster-ai): Use `get_task` or `analyze_project_complexity` *only if explicitly instructed* by Boomerang in the delegation message to gather further context beyond what was provided.
* `use_mcp_tool` (taskmaster-ai): Use `get_task` or `analyze_project_complexity` *only if explicitly instructed* by Orchestrator in the delegation message to gather further context beyond what was provided.
3. **Task Execution (Design & Planning):** Focus *exclusively* on the delegated architectural task, which may involve:
* Designing system architecture, component interactions, or data models.
* Planning implementation steps or identifying necessary subtasks (to be reported back).
* Analyzing technical feasibility, complexity, or potential risks.
* Defining interfaces, APIs, or data contracts.
* Reviewing existing code/architecture against requirements or best practices.
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include:
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of design decisions, plans created, analysis performed, or subtasks identified.
* Any relevant artifacts produced (e.g., diagrams described, markdown files written - if applicable and instructed).
* Completion status (success, failure, needs review).
* Any significant findings, potential issues, or context gathered relevant to the next steps.
5. **Handling Issues:**
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring further review (e.g., needing testing input, deeper debugging analysis), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang.
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring further review (e.g., needing testing input, deeper debugging analysis), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the task fails (e.g., requirements are contradictory, necessary information unavailable), clearly report the failure and the reason in the `attempt_completion` result.
6. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message.
7. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
* **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
7. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:**
@@ -42,17 +42,17 @@ context_reporting: |
<thinking>
Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively.
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of architectural decisions, plans, analysis, identified subtasks, errors encountered, or new context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates.
- **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang).
# Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: |
@@ -64,7 +64,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow."
2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'.
@@ -73,21 +73,21 @@ taskmaster_strategy:
**Mode Collaboration & Triggers (Architect Perspective):**
mode_collaboration: |
# Architect Mode Collaboration (Focus on receiving from Boomerang and reporting back)
- Delegated Task Reception (FROM Boomerang via `new_task`):
# Architect Mode Collaboration (Focus on receiving from Orchestrator and reporting back)
- Delegated Task Reception (FROM Orchestrator via `new_task`):
* Receive specific architectural/planning task instructions referencing a `taskmaster-ai` ID.
* Analyze requirements, scope, and constraints provided by Boomerang.
- Completion Reporting (TO Boomerang via `attempt_completion`):
* Analyze requirements, scope, and constraints provided by Orchestrator.
- Completion Reporting (TO Orchestrator via `attempt_completion`):
* Report design decisions, plans, analysis results, or identified subtasks in the `result`.
* Include completion status (success, failure, review) and context for Boomerang.
* Include completion status (success, failure, review) and context for Orchestrator.
* Signal completion of the *specific delegated architectural task*.
mode_triggers:
# Conditions that might trigger a switch TO Architect mode (typically orchestrated BY Boomerang based on needs identified by other modes or the user)
# Conditions that might trigger a switch TO Architect mode (typically orchestrated BY Orchestrator based on needs identified by other modes or the user)
architect:
- condition: needs_architectural_design # e.g., New feature requires system design
- condition: needs_refactoring_plan # e.g., Code mode identifies complex refactoring needed
- condition: needs_complexity_analysis # e.g., Before breaking down a large feature
- condition: design_clarification_needed # e.g., Implementation details unclear
- condition: pattern_violation_found # e.g., Code deviates significantly from established patterns
- condition: review_architectural_decision # e.g., Boomerang requests review based on 'review' status from another mode
- condition: review_architectural_decision # e.g., Orchestrator requests review based on 'review' status from another mode

View File

@@ -9,16 +9,16 @@
**Information Retrieval & Explanation Role (Delegated Tasks):**
Your primary role when activated via `new_task` by the Boomerang (orchestrator) mode is to act as a specialized technical assistant. Focus *exclusively* on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
Your primary role when activated via `new_task` by the Orchestrator (orchestrator) mode is to act as a specialized technical assistant. Focus *exclusively* on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Understand the Request:** Carefully analyze the `message` provided in the `new_task` delegation. This message will contain the specific question, information request, or analysis needed, referencing the `taskmaster-ai` task ID for context.
2. **Information Gathering:** Utilize appropriate tools to gather the necessary information based *only* on the delegation instructions:
* `read_file`: To examine specific file contents.
* `search_files`: To find patterns or specific text across the project.
* `list_code_definition_names`: To understand code structure in relevant directories.
* `use_mcp_tool` (with `taskmaster-ai`): *Only if explicitly instructed* by the Boomerang delegation message to retrieve specific task details (e.g., using `get_task`).
* `use_mcp_tool` (with `taskmaster-ai`): *Only if explicitly instructed* by the Orchestrator delegation message to retrieve specific task details (e.g., using `get_task`).
3. **Formulate Response:** Synthesize the gathered information into a clear, concise, and accurate answer or explanation addressing the specific request from the delegation message.
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to process and potentially update `taskmaster-ai`. Include:
4. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to process and potentially update `taskmaster-ai`. Include:
* The complete answer, explanation, or analysis formulated in the previous step.
* Completion status (success, failure - e.g., if information could not be found).
* Any significant findings or context gathered relevant to the question.
@@ -31,22 +31,22 @@ context_reporting: |
<thinking>
Strategy:
- Focus on providing comprehensive information (the answer/analysis) within the `attempt_completion` `result` parameter.
- Boomerang will use this information to potentially update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- Orchestrator will use this information to potentially update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster.
</thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains the complete and accurate answer/analysis requested by Boomerang.
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains the complete and accurate answer/analysis requested by Orchestrator.
- **Content:** Include the full answer, explanation, or analysis results. Cite sources if applicable. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs any necessary Taskmaster updates or decides the next workflow step.
- **Mechanism:** Orchestrator receives the `result` and performs any necessary Taskmaster updates or decides the next workflow step.
**Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Use (Rare & Specific):** Only use Taskmaster tools (`use_mcp_tool` with `taskmaster-ai`) if *explicitly instructed* by Boomerang within the `new_task` message, and *only* for retrieving information (e.g., `get_task`). Do not update Taskmaster status or content directly.
* **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Use (Rare & Specific):** Only use Taskmaster tools (`use_mcp_tool` with `taskmaster-ai`) if *explicitly instructed* by Orchestrator within the `new_task` message, and *only* for retrieving information (e.g., `get_task`). Do not update Taskmaster status or content directly.
**Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang), which is highly exceptional for Ask mode.
# Only relevant if operating autonomously (not delegated by Orchestrator), which is highly exceptional for Ask mode.
taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: |
@@ -58,7 +58,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow."
2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context (again, very rare for Ask).
2. **Set Status:** Set status to '[TASKMASTER: ON]'.
@@ -67,13 +67,13 @@ taskmaster_strategy:
**Mode Collaboration & Triggers:**
mode_collaboration: |
# Ask Mode Collaboration: Focuses on receiving tasks from Boomerang and reporting back findings.
- Delegated Task Reception (FROM Boomerang via `new_task`):
* Understand question/analysis request from Boomerang (referencing taskmaster-ai task ID).
# Ask Mode Collaboration: Focuses on receiving tasks from Orchestrator and reporting back findings.
- Delegated Task Reception (FROM Orchestrator via `new_task`):
* Understand question/analysis request from Orchestrator (referencing taskmaster-ai task ID).
* Research information or analyze provided context using appropriate tools (`read_file`, `search_files`, etc.) as instructed.
* Formulate answers/explanations strictly within the subtask scope.
* Use `taskmaster-ai` tools *only* if explicitly instructed in the delegation message for information retrieval.
- Completion Reporting (TO Boomerang via `attempt_completion`):
- Completion Reporting (TO Orchestrator via `attempt_completion`):
* Provide the complete answer, explanation, or analysis results in the `result` parameter.
* Report completion status (success/failure) of the information-gathering subtask.
* Cite sources or relevant context found.

View File

@@ -9,22 +9,22 @@
**Execution Role (Delegated Tasks):**
Your primary role is to **execute** tasks delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
Your primary role is to **execute** tasks delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Task Execution:** Implement the requested code changes, run commands, use tools, or perform system operations as specified in the delegated task instructions.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include:
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Outcome of commands/tool usage.
* Summary of code changes made or system operations performed.
* Completion status (success, failure, needs review).
* Any significant findings, errors encountered, or context gathered.
* Links to commits or relevant code sections if applicable.
3. **Handling Issues:**
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring review (architectural, testing, debugging), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang.
* **Complexity/Review:** If you encounter significant complexity, uncertainty, or issues requiring review (architectural, testing, debugging), set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the task fails, clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
* **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:**
@@ -32,17 +32,17 @@ context_reporting: |
<thinking>
Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively.
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of actions taken, results achieved, errors encountered, decisions made during execution (if relevant to the outcome), and any new context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates.
- **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang).
# Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: |
@@ -54,7 +54,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow."
2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -9,29 +9,29 @@
**Execution Role (Delegated Tasks):**
Your primary role is to **execute diagnostic tasks** delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
Your primary role is to **execute diagnostic tasks** delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID.
1. **Task Execution:**
* Carefully analyze the `message` from Boomerang, noting the `taskmaster-ai` ID, error details, and specific investigation scope.
* Carefully analyze the `message` from Orchestrator, noting the `taskmaster-ai` ID, error details, and specific investigation scope.
* Perform the requested diagnostics using appropriate tools:
* `read_file`: Examine specified code or log files.
* `search_files`: Locate relevant code, errors, or patterns.
* `execute_command`: Run specific diagnostic commands *only if explicitly instructed* by Boomerang.
* `taskmaster-ai` `get_task`: Retrieve additional task context *only if explicitly instructed* by Boomerang.
* `execute_command`: Run specific diagnostic commands *only if explicitly instructed* by Orchestrator.
* `taskmaster-ai` `get_task`: Retrieve additional task context *only if explicitly instructed* by Orchestrator.
* Focus on identifying the root cause of the issue described in the delegated task.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include:
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of diagnostic steps taken and findings (e.g., identified root cause, affected areas).
* Recommended next steps (e.g., specific code changes for Code mode, further tests for Test mode).
* Completion status (success, failure, needs review). Reference the original `taskmaster-ai` task ID.
* Any significant context gathered during the investigation.
* **Crucially:** Execute *only* the delegated diagnostic task. Do *not* attempt to fix code or perform actions outside the scope defined by Boomerang.
* **Crucially:** Execute *only* the delegated diagnostic task. Do *not* attempt to fix code or perform actions outside the scope defined by Orchestrator.
3. **Handling Issues:**
* **Needs Review:** If the root cause is unclear, requires architectural input, or needs further specialized testing, set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Boomerang.
* **Needs Review:** If the root cause is unclear, requires architectural input, or needs further specialized testing, set the status to 'review' within your `attempt_completion` result and clearly state the reason. **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the diagnostic task cannot be completed (e.g., required files missing, commands fail), clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
* **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:**
@@ -39,17 +39,17 @@ context_reporting: |
<thinking>
Strategy:
- Focus on providing comprehensive diagnostic findings within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask` and decide the next step (e.g., delegate fix to Code mode).
- Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask` and decide the next step (e.g., delegate fix to Code mode).
- My role is to *report* diagnostic findings accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary diagnostic information for Boomerang to understand the issue, update Taskmaster, and plan the next action.
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary diagnostic information for Orchestrator to understand the issue, update Taskmaster, and plan the next action.
- **Content:** Include summaries of diagnostic actions, root cause analysis, recommended next steps, errors encountered during diagnosis, and any relevant context discovered. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates and subsequent delegation.
- **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates and subsequent delegation.
**Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang).
# Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: |
@@ -61,7 +61,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow."
2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -70,52 +70,52 @@ taskmaster_strategy:
**Mode Collaboration & Triggers:**
mode_collaboration: |
# Collaboration definitions for how Boomerang orchestrates and interacts.
# Boomerang delegates via `new_task` using taskmaster-ai for task context,
# Collaboration definitions for how Orchestrator orchestrates and interacts.
# Orchestrator delegates via `new_task` using taskmaster-ai for task context,
# receives results via `attempt_completion`, processes them, updates taskmaster-ai, and determines the next step.
1. Architect Mode Collaboration: # Interaction initiated BY Boomerang
1. Architect Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`:
* Provide clear architectural task scope (referencing taskmaster-ai task ID).
* Request design, structure, planning based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Architect via attempt_completion
- Completion Reporting TO Orchestrator: # Receiving results FROM Architect via attempt_completion
* Expect design decisions, artifacts created, completion status (taskmaster-ai task ID).
* Expect context needed for subsequent implementation delegation.
2. Test Mode Collaboration: # Interaction initiated BY Boomerang
2. Test Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`:
* Provide clear testing scope (referencing taskmaster-ai task ID).
* Request test plan development, execution, verification based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Test via attempt_completion
- Completion Reporting TO Orchestrator: # Receiving results FROM Test via attempt_completion
* Expect summary of test results (pass/fail, coverage), completion status (taskmaster-ai task ID).
* Expect details on bugs or validation issues.
3. Debug Mode Collaboration: # Interaction initiated BY Boomerang
3. Debug Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`:
* Provide clear debugging scope (referencing taskmaster-ai task ID).
* Request investigation, root cause analysis based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Debug via attempt_completion
- Completion Reporting TO Orchestrator: # Receiving results FROM Debug via attempt_completion
* Expect summary of findings (root cause, affected areas), completion status (taskmaster-ai task ID).
* Expect recommended fixes or next diagnostic steps.
4. Ask Mode Collaboration: # Interaction initiated BY Boomerang
4. Ask Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`:
* Provide clear question/analysis request (referencing taskmaster-ai task ID).
* Request research, context analysis, explanation based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Ask via attempt_completion
- Completion Reporting TO Orchestrator: # Receiving results FROM Ask via attempt_completion
* Expect answers, explanations, analysis results, completion status (taskmaster-ai task ID).
* Expect cited sources or relevant context found.
5. Code Mode Collaboration: # Interaction initiated BY Boomerang
5. Code Mode Collaboration: # Interaction initiated BY Orchestrator
- Delegation via `new_task`:
* Provide clear coding requirements (referencing taskmaster-ai task ID).
* Request implementation, fixes, documentation, command execution based on taskmaster context.
- Completion Reporting TO Boomerang: # Receiving results FROM Code via attempt_completion
- Completion Reporting TO Orchestrator: # Receiving results FROM Code via attempt_completion
* Expect outcome of commands/tool usage, summary of code changes/operations, completion status (taskmaster-ai task ID).
* Expect links to commits or relevant code sections if relevant.
7. Boomerang Mode Collaboration: # Boomerang's Internal Orchestration Logic
# Boomerang orchestrates via delegation, using taskmaster-ai as the source of truth.
7. Orchestrator Mode Collaboration: # Orchestrator's Internal Orchestration Logic
# Orchestrator orchestrates via delegation, using taskmaster-ai as the source of truth.
- Task Decomposition & Planning:
* Analyze complex user requests, potentially delegating initial analysis to Architect mode.
* Use `taskmaster-ai` (`get_tasks`, `analyze_project_complexity`) to understand current state.
@@ -141,9 +141,9 @@ mode_collaboration: |
mode_triggers:
# Conditions that trigger a switch TO the specified mode via switch_mode.
# Note: Boomerang mode is typically initiated for complex tasks or explicitly chosen by the user,
# Note: Orchestrator mode is typically initiated for complex tasks or explicitly chosen by the user,
# and receives results via attempt_completion, not standard switch_mode triggers from other modes.
# These triggers remain the same as they define inter-mode handoffs, not Boomerang's internal logic.
# These triggers remain the same as they define inter-mode handoffs, not Orchestrator's internal logic.
architect:
- condition: needs_architectural_changes

View File

@@ -9,22 +9,22 @@
**Execution Role (Delegated Tasks):**
Your primary role is to **execute** testing tasks delegated to you by the Boomerang orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID and its associated context (e.g., `testStrategy`).
Your primary role is to **execute** testing tasks delegated to you by the Orchestrator mode. Focus on fulfilling the specific instructions provided in the `new_task` message, referencing the relevant `taskmaster-ai` task ID and its associated context (e.g., `testStrategy`).
1. **Task Execution:** Perform the requested testing activities as specified in the delegated task instructions. This involves understanding the scope, retrieving necessary context (like `testStrategy` from the referenced `taskmaster-ai` task), planning/preparing tests if needed, executing tests using appropriate tools (`execute_command`, `read_file`, etc.), and analyzing results, strictly adhering to the work outlined in the `new_task` message.
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Boomerang to update `taskmaster-ai`. Include:
2. **Reporting Completion:** Signal completion using `attempt_completion`. Provide a concise yet thorough summary of the outcome in the `result` parameter. This summary is **crucial** for Orchestrator to update `taskmaster-ai`. Include:
* Summary of testing activities performed (e.g., tests planned, executed).
* Concise results/outcome (e.g., pass/fail counts, overall status, coverage information if applicable).
* Completion status (success, failure, needs review - e.g., if tests reveal significant issues needing broader attention).
* Any significant findings (e.g., details of bugs, errors, or validation issues found).
* Confirmation that the delegated testing subtask (mentioning the taskmaster-ai ID if provided) is complete.
3. **Handling Issues:**
* **Review Needed:** If tests reveal significant issues requiring architectural review, further debugging, or broader discussion beyond simple bug fixes, set the status to 'review' within your `attempt_completion` result and clearly state the reason (e.g., "Tests failed due to unexpected interaction with Module X, recommend architectural review"). **Do not delegate directly.** Report back to Boomerang.
* **Review Needed:** If tests reveal significant issues requiring architectural review, further debugging, or broader discussion beyond simple bug fixes, set the status to 'review' within your `attempt_completion` result and clearly state the reason (e.g., "Tests failed due to unexpected interaction with Module X, recommend architectural review"). **Do not delegate directly.** Report back to Orchestrator.
* **Failure:** If the testing task itself cannot be completed (e.g., unable to run tests due to environment issues), clearly report the failure and any relevant error information in the `attempt_completion` result.
4. **Taskmaster Interaction:**
* **Primary Responsibility:** Boomerang is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Boomerang's delegation) or if *explicitly* instructed by Boomerang within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Boomerang's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
* **Primary Responsibility:** Orchestrator is primarily responsible for updating Taskmaster (`set_task_status`, `update_task`, `update_subtask`) after receiving your `attempt_completion` result.
* **Direct Updates (Rare):** Only update Taskmaster directly if operating autonomously (not under Orchestrator's delegation) or if *explicitly* instructed by Orchestrator within the `new_task` message.
5. **Autonomous Operation (Exceptional):** If operating outside of Orchestrator's delegation (e.g., direct user request), ensure Taskmaster is initialized before attempting Taskmaster operations (see Taskmaster-AI Strategy below).
**Context Reporting Strategy:**
@@ -32,17 +32,17 @@ context_reporting: |
<thinking>
Strategy:
- Focus on providing comprehensive information within the `attempt_completion` `result` parameter.
- Boomerang will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- Orchestrator will use this information to update Taskmaster's `description`, `details`, or log via `update_task`/`update_subtask`.
- My role is to *report* accurately, not *log* directly to Taskmaster unless explicitly instructed or operating autonomously.
</thinking>
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Boomerang to understand the outcome and update Taskmaster effectively.
- **Goal:** Ensure the `result` parameter in `attempt_completion` contains all necessary information for Orchestrator to understand the outcome and update Taskmaster effectively.
- **Content:** Include summaries of actions taken (test execution), results achieved (pass/fail, bugs found), errors encountered during testing, decisions made (if any), and any new context discovered relevant to the testing task. Structure the `result` clearly.
- **Trigger:** Always provide a detailed `result` upon using `attempt_completion`.
- **Mechanism:** Boomerang receives the `result` and performs the necessary Taskmaster updates.
- **Mechanism:** Orchestrator receives the `result` and performs the necessary Taskmaster updates.
**Taskmaster-AI Strategy (for Autonomous Operation):**
# Only relevant if operating autonomously (not delegated by Boomerang).
# Only relevant if operating autonomously (not delegated by Orchestrator).
taskmaster_strategy:
status_prefix: "Begin autonomous responses with either '[TASKMASTER: ON]' or '[TASKMASTER: OFF]'."
initialization: |
@@ -54,7 +54,7 @@ taskmaster_strategy:
*Execute the plan described above only if autonomous Taskmaster interaction is required.*
if_uninitialized: |
1. **Inform:** "Task Master is not initialized. Autonomous Taskmaster operations cannot proceed."
2. **Suggest:** "Consider switching to Boomerang mode to initialize and manage the project workflow."
2. **Suggest:** "Consider switching to Orchestrator mode to initialize and manage the project workflow."
if_ready: |
1. **Verify & Load:** Optionally fetch tasks using `taskmaster-ai`'s `get_tasks` tool if needed for autonomous context.
2. **Set Status:** Set status to '[TASKMASTER: ON]'.

View File

@@ -72,6 +72,7 @@ Taskmaster uses two primary methods for configuration:
- `XAI_API_KEY`: Your X-AI API key.
- **Optional Endpoint Overrides:**
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
- `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role).
- `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`).
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
@@ -131,13 +132,14 @@ PERPLEXITY_API_KEY=pplx-your-key-here
# etc.
# Optional Endpoint Overrides
# Use a specific provider's base URL, e.g., for an OpenAI-compatible API
# OPENAI_BASE_URL=https://api.third-party.com/v1
#
# AZURE_OPENAI_ENDPOINT=https://your-azure-endpoint.openai.azure.com/
# OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
# Google Vertex AI Configuration (Required if using 'vertex' provider)
# VERTEX_PROJECT_ID=your-gcp-project-id
# VERTEX_LOCATION=us-central1
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json
```
## Troubleshooting

View File

@@ -83,6 +83,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
.option('--skip-install', 'Skip installing dependencies')
.option('--dry-run', 'Show what would be done without making changes')
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
.option('--git', 'Initialize Git repository')
.option('--no-git', 'Skip Git repository initialization')
.option('--git-tasks', 'Store tasks in Git')
.option('--no-git-tasks', 'No Git storage of tasks')
.action(async (cmdOptions) => {
try {
await runInitCLI(cmdOptions);

View File

@@ -11,7 +11,7 @@ import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transf
/**
* Direct function wrapper for initializing a project.
* Derives target directory from session, sets CWD, and calls core init logic.
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot, rules)
* @param {object} args - Arguments containing initialization options (addAliases, initGit, storeTasksInGit, skipInstall, yes, projectRoot, rules)
* @param {object} log - The FastMCP logger instance.
* @param {object} context - The context object, must contain { session }.
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
@@ -65,7 +65,9 @@ export async function initializeProjectDirect(args, log, context = {}) {
// Construct options ONLY from the relevant flags in args
// The core initializeProject operates in the current CWD, which we just set
const options = {
aliases: args.addAliases,
addAliases: args.addAliases,
initGit: args.initGit,
storeTasksInGit: args.storeTasksInGit,
skipInstall: args.skipInstall,
yes: true // Force yes mode
};

View File

@@ -23,8 +23,18 @@ export function registerInitializeProjectTool(server) {
addAliases: z
.boolean()
.optional()
.default(false)
.default(true)
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
initGit: z
.boolean()
.optional()
.default(true)
.describe('Initialize Git repository in project root.'),
storeTasksInGit: z
.boolean()
.optional()
.default(false)
.describe('Store tasks in Git (tasks.json and tasks/ directory).'),
yes: z
.boolean()
.optional()

1673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "task-master-ai",
"version": "0.17.1",
"version": "0.17.1-test",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
"bin": {
"task-master": "bin/task-master.js",
"task-master-mcp": "mcp-server/server.js",
"task-master-ai": "mcp-server/server.js"
"task-master": "dist/task-master.cjs",
"task-master-mcp": "dist/task-master-mcp.cjs",
"task-master-ai": "dist/task-master-mcp.cjs"
},
"scripts": {
"test": "node --experimental-vm-modules node_modules/.bin/jest",
@@ -22,7 +22,13 @@
"inspector": "npx @modelcontextprotocol/inspector node mcp-server/server.js",
"mcp-server": "node mcp-server/server.js",
"format-check": "biome format .",
"format": "biome format . --write"
"format": "biome format . --write",
"prebuild": "npm test",
"build": "vite build && npm run postbuild",
"postbuild": "node scripts/add-shebang.js",
"build:bundle": "npm run build",
"build:watch": "vite build --watch",
"test:build": "echo 'Testing bundled binaries:' && echo '🔧 CLI (task-master):' && node dist/task-master.cjs --version && echo '🔌 MCP (task-master-ai):' && node dist/task-master-mcp.cjs --help | head -5 && echo '✅ Both bundles work!'"
},
"keywords": [
"claude",
@@ -91,14 +97,11 @@
"url": "https://github.com/eyaltoledano/claude-task-master/issues"
},
"files": [
"scripts/**",
"assets/**",
".cursor/**",
"README-task-master.md",
"index.js",
"bin/**",
"dist/**",
"mcp-server/**",
"src/**"
"README-task-master.md",
".cursor/**",
"assets/**"
],
"overrides": {
"node-fetch": "^2.6.12",
@@ -108,15 +111,19 @@
"@biomejs/biome": "^1.9.4",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.28.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@types/jest": "^29.5.14",
"execa": "^8.0.1",
"ink": "^5.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
"mock-fs": "^5.5.0",
"pkg": "^5.8.1",
"prettier": "^3.5.3",
"react": "^18.3.1",
"supertest": "^7.1.0",
"tsx": "^4.16.2"
"tsx": "^4.16.2",
"vite": "^6.3.5"
}
}
}

82
scripts/add-shebang.js Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
import {
readFileSync,
writeFileSync,
chmodSync,
copyFileSync,
mkdirSync,
existsSync
} from 'fs';
import { join, dirname } from 'path';
const bundlePaths = [
join(process.cwd(), 'dist/task-master.cjs'), // CLI tool
join(process.cwd(), 'dist/task-master-mcp.cjs') // MCP server
];
try {
// Copy necessary asset files to dist
const assetsToCopy = [
{
src: 'scripts/modules/supported-models.json',
dest: 'dist/supported-models.json'
},
{ src: 'README-task-master.md', dest: 'dist/README-task-master.md' }
];
console.log('📁 Copying assets...');
for (const asset of assetsToCopy) {
const srcPath = join(process.cwd(), asset.src);
const destPath = join(process.cwd(), asset.dest);
if (existsSync(srcPath)) {
// Ensure destination directory exists
const destDir = dirname(destPath);
if (!existsSync(destDir)) {
mkdirSync(destDir, { recursive: true });
}
copyFileSync(srcPath, destPath);
console.log(` ✅ Copied ${asset.src}${asset.dest}`);
} else {
console.log(` ⚠️ Source not found: ${asset.src}`);
}
}
// Process each bundle file
for (const bundlePath of bundlePaths) {
const fileName = bundlePath.split('/').pop();
if (!existsSync(bundlePath)) {
console.log(`⚠️ Bundle not found: ${fileName}`);
continue;
}
// Read the existing bundle
const bundleContent = readFileSync(bundlePath, 'utf8');
// Add shebang if it doesn't already exist
if (!bundleContent.startsWith('#!/usr/bin/env node')) {
const contentWithShebang = '#!/usr/bin/env node\n' + bundleContent;
writeFileSync(bundlePath, contentWithShebang);
console.log(`✅ Added shebang to ${fileName}`);
} else {
console.log(`✅ Shebang already exists in ${fileName}`);
}
// Make it executable
chmodSync(bundlePath, 0o755);
console.log(`✅ Made ${fileName} executable`);
}
console.log('📦 Both bundles ready:');
console.log(' 🔧 CLI tool: dist/task-master.cjs');
console.log(' 🔌 MCP server: dist/task-master-mcp.cjs');
console.log('🧪 Test with:');
console.log(' node dist/task-master.cjs --version');
console.log(' node dist/task-master-mcp.cjs --help');
} catch (error) {
console.error('❌ Post-build failed:', error.message);
process.exit(1);
}

View File

@@ -23,6 +23,8 @@ import figlet from 'figlet';
import boxen from 'boxen';
import gradient from 'gradient-string';
import { isSilentMode } from './modules/utils.js';
import { insideGitWorkTree } from './modules/utils/git-utils.js';
import { manageGitignoreFile } from '../src/utils/manage-gitignore.js';
import { RULE_PROFILES } from '../src/constants/profiles.js';
import {
convertAllRulesToProfileRules,
@@ -320,6 +322,30 @@ async function initializeProject(options = {}) {
// console.log('==================================================');
// }
// Handle boolean aliases flags
if (options.aliases === true) {
options.addAliases = true; // --aliases flag provided
} else if (options.aliases === false) {
options.addAliases = false; // --no-aliases flag provided
}
// If options.aliases and options.noAliases are undefined, we'll prompt for it
// Handle boolean git flags
if (options.git === true) {
options.initGit = true; // --git flag provided
} else if (options.git === false) {
options.initGit = false; // --no-git flag provided
}
// If options.git and options.noGit are undefined, we'll prompt for it
// Handle boolean gitTasks flags
if (options.gitTasks === true) {
options.storeTasksInGit = true; // --git-tasks flag provided
} else if (options.gitTasks === false) {
options.storeTasksInGit = false; // --no-git-tasks flag provided
}
// If options.gitTasks and options.noGitTasks are undefined, we'll prompt for it
const skipPrompts = options.yes || (options.name && options.description);
// if (!isSilentMode()) {
@@ -343,21 +369,44 @@ async function initializeProject(options = {}) {
const projectVersion = options.version || '0.1.0';
const authorName = options.author || 'Vibe coder';
const dryRun = options.dryRun || false;
const addAliases = options.aliases || false;
const addAliases =
options.addAliases !== undefined ? options.addAliases : true; // Default to true if not specified
const initGit = options.initGit !== undefined ? options.initGit : true; // Default to true if not specified
const storeTasksInGit =
options.storeTasksInGit !== undefined ? options.storeTasksInGit : false; // Default to false if not specified
if (dryRun) {
log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project');
log('info', 'Would create/update necessary project files');
if (addAliases) {
log('info', 'Would add shell aliases for task-master');
}
// Show flag-specific behavior
log(
'info',
`${addAliases ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
);
log(
'info',
`${initGit ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
);
log(
'info',
`${storeTasksInGit ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
);
return {
dryRun: true
};
}
createProjectStructure(addAliases, dryRun, options, selectedRuleProfiles);
createProjectStructure(
addAliases,
initGit,
storeTasksInGit,
dryRun,
options,
selectedRuleProfiles
);
} else {
// Interactive logic
log('info', 'Required options not provided, proceeding with prompts.');
@@ -367,14 +416,45 @@ async function initializeProject(options = {}) {
input: process.stdin,
output: process.stdout
});
// Only prompt for shell aliases
const addAliasesInput = await promptQuestion(
rl,
chalk.cyan(
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
)
);
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
// Prompt for shell aliases (skip if --aliases or --no-aliases flag was provided)
let addAliasesPrompted = true; // Default to true
if (options.addAliases !== undefined) {
addAliasesPrompted = options.addAliases; // Use flag value if provided
} else {
const addAliasesInput = await promptQuestion(
rl,
chalk.cyan(
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
)
);
addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
}
// Prompt for Git initialization (skip if --git or --no-git flag was provided)
let initGitPrompted = true; // Default to true
if (options.initGit !== undefined) {
initGitPrompted = options.initGit; // Use flag value if provided
} else {
const gitInitInput = await promptQuestion(
rl,
chalk.cyan('Initialize a Git repository in project root? (Y/n): ')
);
initGitPrompted = gitInitInput.trim().toLowerCase() !== 'n';
}
// Prompt for Git tasks storage (skip if --git-tasks or --no-git-tasks flag was provided)
let storeGitPrompted = false; // Default to false
if (options.storeTasksInGit !== undefined) {
storeGitPrompted = options.storeTasksInGit; // Use flag value if provided
} else {
const gitTasksInput = await promptQuestion(
rl,
chalk.cyan(
'Store tasks in Git (tasks.json and tasks/ directory)? (y/N): '
)
);
storeGitPrompted = gitTasksInput.trim().toLowerCase() === 'y';
}
// Confirm settings...
console.log('\nTask Master Project settings:');
@@ -384,6 +464,14 @@ async function initializeProject(options = {}) {
),
chalk.white(addAliasesPrompted ? 'Yes' : 'No')
);
console.log(
chalk.blue('Initialize Git repository in project root:'),
chalk.white(initGitPrompted ? 'Yes' : 'No')
);
console.log(
chalk.blue('Store tasks in Git (tasks.json and tasks/ directory):'),
chalk.white(storeGitPrompted ? 'Yes' : 'No')
);
const confirmInput = await promptQuestion(
rl,
@@ -422,9 +510,21 @@ async function initializeProject(options = {}) {
log('info', 'DRY RUN MODE: No files will be modified');
log('info', 'Would initialize Task Master project');
log('info', 'Would create/update necessary project files');
if (addAliasesPrompted) {
log('info', 'Would add shell aliases for task-master');
}
// Show flag-specific behavior
log(
'info',
`${addAliasesPrompted ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
);
log(
'info',
`${initGitPrompted ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
);
log(
'info',
`${storeGitPrompted ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
);
return {
dryRun: true
};
@@ -433,6 +533,8 @@ async function initializeProject(options = {}) {
// Create structure using only necessary values
createProjectStructure(
addAliasesPrompted,
initGitPrompted,
storeGitPrompted,
dryRun,
options,
selectedRuleProfiles
@@ -458,6 +560,8 @@ function promptQuestion(rl, question) {
// Function to create the project structure
function createProjectStructure(
addAliases,
initGit,
storeTasksInGit,
dryRun,
options,
selectedRuleProfiles = RULE_PROFILES // Default to all rule profiles
@@ -507,18 +611,55 @@ function createProjectStructure(
}
);
// Copy .gitignore
copyTemplateFile('gitignore', path.join(targetDir, GITIGNORE_FILE));
// Copy .gitignore with GitTasks preference
try {
const gitignoreTemplatePath = path.join(
__dirname,
'..',
'assets',
'gitignore'
);
const templateContent = fs.readFileSync(gitignoreTemplatePath, 'utf8');
manageGitignoreFile(
path.join(targetDir, GITIGNORE_FILE),
templateContent,
storeTasksInGit,
log
);
} catch (error) {
log('error', `Failed to create .gitignore: ${error.message}`);
}
// Copy example_prd.txt to NEW location
copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
// Initialize git repository if git is available
try {
if (!fs.existsSync(path.join(targetDir, '.git'))) {
log('info', 'Initializing git repository...');
execSync('git init', { stdio: 'ignore' });
log('success', 'Git repository initialized');
if (initGit === false) {
log('info', 'Git initialization skipped due to --no-git flag.');
} else if (initGit === true) {
if (insideGitWorkTree()) {
log(
'info',
'Existing Git repository detected skipping git init despite --git flag.'
);
} else {
log('info', 'Initializing Git repository due to --git flag...');
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
log('success', 'Git repository initialized');
}
} else {
// Default behavior when no flag is provided (from interactive prompt)
if (insideGitWorkTree()) {
log('info', 'Existing Git repository detected skipping git init.');
} else {
log(
'info',
'No Git repository detected. Initializing one in project root...'
);
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
log('success', 'Git repository initialized');
}
}
} catch (error) {
log('warn', 'Git not available, skipping repository initialization');
@@ -599,6 +740,17 @@ function createProjectStructure(
}
// ====================================
// Add shell aliases if requested
if (addAliases && !dryRun) {
log('info', 'Adding shell aliases...');
const aliasResult = addShellAliases();
if (aliasResult) {
log('success', 'Shell aliases added successfully');
}
} else if (addAliases && dryRun) {
log('info', 'DRY RUN: Would add shell aliases (tm, taskmaster)');
}
// Display success message
if (!isSilentMode()) {
console.log(

View File

@@ -3342,6 +3342,11 @@ ${result.result}
.option('--skip-install', 'Skip installing dependencies')
.option('--dry-run', 'Show what would be done without making changes')
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
.option('--git', 'Initialize Git repository')
.option('--no-git', 'Skip Git repository initialization')
.option('--git-tasks', 'Store tasks in Git')
.option('--no-git-tasks', 'No Git storage of tasks')
.action(async (cmdOptions) => {
// cmdOptions contains parsed arguments
// Parse rules: accept space or comma separated, default to all available rules

View File

@@ -571,10 +571,11 @@ function getMcpApiKeyStatus(providerName, projectRoot = null) {
const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
const mcpConfig = JSON.parse(mcpConfigRaw);
const mcpEnv = mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
const mcpEnv =
mcpConfig?.mcpServers?.['task-master-ai']?.env ||
mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
if (!mcpEnv) {
// console.warn(chalk.yellow('Warning: Could not find taskmaster-ai env in mcp.json.'));
return false; // Structure missing
return false;
}
let apiKeyToCheck = null;
@@ -782,9 +783,15 @@ function getAllProviders() {
function getBaseUrlForRole(role, explicitRoot = null) {
const roleConfig = getModelConfigForRole(role, explicitRoot);
return roleConfig && typeof roleConfig.baseURL === 'string'
? roleConfig.baseURL
: undefined;
if (roleConfig && typeof roleConfig.baseURL === 'string') {
return roleConfig.baseURL;
}
const provider = roleConfig?.provider;
if (provider) {
const envVarName = `${provider.toUpperCase()}_BASE_URL`;
return resolveEnvVariable(envVarName, null, explicitRoot);
}
return undefined;
}
export {

View File

@@ -349,6 +349,25 @@ function getCurrentBranchSync(projectRoot) {
}
}
/**
* Check if the current working directory is inside a Git work-tree.
* Uses `git rev-parse --is-inside-work-tree` which is more specific than --git-dir
* for detecting work-trees (excludes bare repos and .git directories).
* This is ideal for preventing accidental git init in existing work-trees.
* @returns {boolean} True if inside a Git work-tree, false otherwise.
*/
function insideGitWorkTree() {
try {
execSync('git rev-parse --is-inside-work-tree', {
stdio: 'ignore',
cwd: process.cwd()
});
return true;
} catch {
return false;
}
}
// Export all functions
export {
isGitRepository,
@@ -366,5 +385,6 @@ export {
checkAndAutoSwitchGitTag,
checkAndAutoSwitchGitTagSync,
isGitRepositorySync,
getCurrentBranchSync
getCurrentBranchSync,
insideGitWorkTree
};

View File

@@ -0,0 +1,293 @@
// Utility to manage .gitignore files with task file preferences and template merging
import fs from 'fs';
import path from 'path';
// Constants
const TASK_FILES_COMMENT = '# Task files';
const TASK_JSON_PATTERN = 'tasks.json';
const TASK_DIR_PATTERN = 'tasks/';
/**
* Normalizes a line by removing comments and trimming whitespace
* @param {string} line - Line to normalize
* @returns {string} Normalized line
*/
function normalizeLine(line) {
return line.trim().replace(/^#/, '').trim();
}
/**
* Checks if a line is task-related (tasks.json or tasks/)
* @param {string} line - Line to check
* @returns {boolean} True if line is task-related
*/
function isTaskLine(line) {
const normalized = normalizeLine(line);
return normalized === TASK_JSON_PATTERN || normalized === TASK_DIR_PATTERN;
}
/**
* Adjusts task-related lines in template based on storage preference
* @param {string[]} templateLines - Array of template lines
* @param {boolean} storeTasksInGit - Whether to comment out task lines
* @returns {string[]} Adjusted template lines
*/
function adjustTaskLinesInTemplate(templateLines, storeTasksInGit) {
return templateLines.map((line) => {
if (isTaskLine(line)) {
const normalized = normalizeLine(line);
// Preserve original trailing whitespace from the line
const originalTrailingSpace = line.match(/\s*$/)[0];
return storeTasksInGit
? `# ${normalized}${originalTrailingSpace}`
: `${normalized}${originalTrailingSpace}`;
}
return line;
});
}
/**
* Removes existing task files section from content
* @param {string[]} existingLines - Existing file lines
* @returns {string[]} Lines with task section removed
*/
function removeExistingTaskSection(existingLines) {
const cleanedLines = [];
let inTaskSection = false;
for (const line of existingLines) {
// Start of task files section
if (line.trim() === TASK_FILES_COMMENT) {
inTaskSection = true;
continue;
}
// Task lines (commented or not)
if (isTaskLine(line)) {
continue;
}
// Empty lines within task section
if (inTaskSection && !line.trim()) {
continue;
}
// End of task section (any non-empty, non-task line)
if (inTaskSection && line.trim() && !isTaskLine(line)) {
inTaskSection = false;
}
// Keep all other lines
if (!inTaskSection) {
cleanedLines.push(line);
}
}
return cleanedLines;
}
/**
* Filters template lines to only include new content not already present
* @param {string[]} templateLines - Template lines
* @param {Set<string>} existingLinesSet - Set of existing trimmed lines
* @returns {string[]} New lines to add
*/
function filterNewTemplateLines(templateLines, existingLinesSet) {
return templateLines.filter((line) => {
const trimmed = line.trim();
if (!trimmed) return false;
// Skip task-related lines (handled separately)
if (isTaskLine(line) || trimmed === TASK_FILES_COMMENT) {
return false;
}
// Include only if not already present
return !existingLinesSet.has(trimmed);
});
}
/**
* Builds the task files section based on storage preference
* @param {boolean} storeTasksInGit - Whether to comment out task lines
* @returns {string[]} Task files section lines
*/
function buildTaskFilesSection(storeTasksInGit) {
const section = [TASK_FILES_COMMENT];
if (storeTasksInGit) {
section.push(`# ${TASK_JSON_PATTERN}`, `# ${TASK_DIR_PATTERN} `);
} else {
section.push(TASK_JSON_PATTERN, `${TASK_DIR_PATTERN} `);
}
return section;
}
/**
* Adds a separator line if needed (avoids double spacing)
* @param {string[]} lines - Current lines array
*/
function addSeparatorIfNeeded(lines) {
if (lines.some((line) => line.trim())) {
const lastLine = lines[lines.length - 1];
if (lastLine && lastLine.trim()) {
lines.push('');
}
}
}
/**
* Validates input parameters
* @param {string} targetPath - Path to .gitignore file
* @param {string} content - Template content
* @param {boolean} storeTasksInGit - Storage preference
* @throws {Error} If validation fails
*/
function validateInputs(targetPath, content, storeTasksInGit) {
if (!targetPath || typeof targetPath !== 'string') {
throw new Error('targetPath must be a non-empty string');
}
if (!targetPath.endsWith('.gitignore')) {
throw new Error('targetPath must end with .gitignore');
}
if (!content || typeof content !== 'string') {
throw new Error('content must be a non-empty string');
}
if (typeof storeTasksInGit !== 'boolean') {
throw new Error('storeTasksInGit must be a boolean');
}
}
/**
* Creates a new .gitignore file from template
* @param {string} targetPath - Path to create file at
* @param {string[]} templateLines - Adjusted template lines
* @param {function} log - Logging function
*/
function createNewGitignoreFile(targetPath, templateLines, log) {
try {
fs.writeFileSync(targetPath, templateLines.join('\n'));
if (typeof log === 'function') {
log('success', `Created ${targetPath} with full template`);
}
} catch (error) {
if (typeof log === 'function') {
log('error', `Failed to create ${targetPath}: ${error.message}`);
}
throw error;
}
}
/**
* Merges template content with existing .gitignore file
* @param {string} targetPath - Path to existing file
* @param {string[]} templateLines - Adjusted template lines
* @param {boolean} storeTasksInGit - Storage preference
* @param {function} log - Logging function
*/
function mergeWithExistingFile(
targetPath,
templateLines,
storeTasksInGit,
log
) {
try {
// Read and process existing file
const existingContent = fs.readFileSync(targetPath, 'utf8');
const existingLines = existingContent.split('\n');
// Remove existing task section
const cleanedExistingLines = removeExistingTaskSection(existingLines);
// Find new template lines to add
const existingLinesSet = new Set(
cleanedExistingLines.map((line) => line.trim()).filter((line) => line)
);
const newLines = filterNewTemplateLines(templateLines, existingLinesSet);
// Build final content
const finalLines = [...cleanedExistingLines];
// Add new template content
if (newLines.length > 0) {
addSeparatorIfNeeded(finalLines);
finalLines.push(...newLines);
}
// Add task files section
addSeparatorIfNeeded(finalLines);
finalLines.push(...buildTaskFilesSection(storeTasksInGit));
// Write result
fs.writeFileSync(targetPath, finalLines.join('\n'));
if (typeof log === 'function') {
const hasNewContent =
newLines.length > 0 ? ' and merged new content' : '';
log(
'success',
`Updated ${targetPath} according to user preference${hasNewContent}`
);
}
} catch (error) {
if (typeof log === 'function') {
log(
'error',
`Failed to merge content with ${targetPath}: ${error.message}`
);
}
throw error;
}
}
/**
* Manages .gitignore file creation and updates with task file preferences
* @param {string} targetPath - Path to the .gitignore file
* @param {string} content - Template content for .gitignore
* @param {boolean} storeTasksInGit - Whether to store tasks in git or not
* @param {function} log - Logging function (level, message)
* @throws {Error} If validation or file operations fail
*/
function manageGitignoreFile(
targetPath,
content,
storeTasksInGit = false,
log = null
) {
// Validate inputs
validateInputs(targetPath, content, storeTasksInGit);
// Process template with task preference
const templateLines = content.split('\n');
const adjustedTemplateLines = adjustTaskLinesInTemplate(
templateLines,
storeTasksInGit
);
// Handle file creation or merging
if (!fs.existsSync(targetPath)) {
createNewGitignoreFile(targetPath, adjustedTemplateLines, log);
} else {
mergeWithExistingFile(
targetPath,
adjustedTemplateLines,
storeTasksInGit,
log
);
}
}
export default manageGitignoreFile;
export {
manageGitignoreFile,
normalizeLine,
isTaskLine,
buildTaskFilesSection,
TASK_FILES_COMMENT,
TASK_JSON_PATTERN,
TASK_DIR_PATTERN
};

View File

@@ -0,0 +1,581 @@
/**
* Integration tests for manage-gitignore.js module
* Tests actual file system operations in a temporary directory
*/
import fs from 'fs';
import path from 'path';
import os from 'os';
import manageGitignoreFile from '../../src/utils/manage-gitignore.js';
describe('manage-gitignore.js Integration Tests', () => {
let tempDir;
let testGitignorePath;
beforeEach(() => {
// Create a temporary directory for each test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
testGitignorePath = path.join(tempDir, '.gitignore');
});
afterEach(() => {
// Clean up temporary directory after each test
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('New File Creation', () => {
const templateContent = `# Logs
logs
*.log
npm-debug.log*
# Dependencies
node_modules/
jspm_packages/
# Environment variables
.env
.env.local
# Task files
tasks.json
tasks/ `;
test('should create new .gitignore file with commented task lines (storeTasksInGit = true)', () => {
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, true, mockLog);
// Verify file was created
expect(fs.existsSync(testGitignorePath)).toBe(true);
// Verify content
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('logs');
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Task files');
expect(content).toContain('tasks.json');
expect(content).toContain('tasks/');
// Verify task lines are commented (storeTasksInGit = true)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Created')
});
});
test('should create new .gitignore file with uncommented task lines (storeTasksInGit = false)', () => {
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
// Verify file was created
expect(fs.existsSync(testGitignorePath)).toBe(true);
// Verify content
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Task files');
// Verify task lines are uncommented (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Created')
});
});
test('should work without log function', () => {
expect(() => {
manageGitignoreFile(testGitignorePath, templateContent, false);
}).not.toThrow();
expect(fs.existsSync(testGitignorePath)).toBe(true);
});
});
describe('File Merging', () => {
const templateContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Environment variables
.env
# Task files
tasks.json
tasks/ `;
test('should merge template with existing file content', () => {
// Create existing .gitignore file
const existingContent = `# Existing content
old-files.txt
*.backup
# Old task files (to be replaced)
# Task files
# tasks.json
# tasks/
# More existing content
cache/`;
fs.writeFileSync(testGitignorePath, existingContent);
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
// Verify file still exists
expect(fs.existsSync(testGitignorePath)).toBe(true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing non-task content
expect(content).toContain('# Existing content');
expect(content).toContain('old-files.txt');
expect(content).toContain('*.backup');
expect(content).toContain('# More existing content');
expect(content).toContain('cache/');
// Should add new template content
expect(content).toContain('# Logs');
expect(content).toContain('logs');
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Environment variables');
expect(content).toContain('.env');
// Should replace task section with new preference (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Verify log message
expect(logs).toContainEqual({
level: 'success',
message: expect.stringContaining('Updated')
});
});
test('should handle switching task preferences from commented to uncommented', () => {
// Create existing file with commented task lines
const existingContent = `# Existing
existing.txt
# Task files
# tasks.json
# tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Update with storeTasksInGit = true (commented)
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
// Should have commented task lines (storeTasksInGit = true)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
test('should handle switching task preferences from uncommented to commented', () => {
// Create existing file with uncommented task lines
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Update with storeTasksInGit = false (uncommented)
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
// Should have uncommented task lines (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should not duplicate existing template content', () => {
// Create existing file that already has some template content
const existingContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Custom content
custom.txt
# Task files
# tasks.json
# tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should not duplicate logs section
const logsMatches = content.match(/# Logs/g);
expect(logsMatches).toHaveLength(1);
// Should not duplicate dependencies section
const depsMatches = content.match(/# Dependencies/g);
expect(depsMatches).toHaveLength(1);
// Should retain custom content
expect(content).toContain('# Custom content');
expect(content).toContain('custom.txt');
// Should add new template content that wasn't present
expect(content).toContain('# Environment variables');
expect(content).toContain('.env');
});
test('should handle empty existing file', () => {
// Create empty file
fs.writeFileSync(testGitignorePath, '');
manageGitignoreFile(testGitignorePath, templateContent, false);
expect(fs.existsSync(testGitignorePath)).toBe(true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('# Task files');
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle file with only whitespace', () => {
// Create file with only whitespace
fs.writeFileSync(testGitignorePath, ' \n\n \n');
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
expect(content).toContain('# Logs');
expect(content).toContain('# Task files');
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
});
describe('Complex Task Section Handling', () => {
test('should remove task section with mixed comments and spacing', () => {
const existingContent = `# Dependencies
node_modules/
# Task files
# tasks.json
tasks/
# More content
more.txt`;
const templateContent = `# New content
new.txt
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain non-task content
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# More content');
expect(content).toContain('more.txt');
// Should add new content
expect(content).toContain('# New content');
expect(content).toContain('new.txt');
// Should have clean task section (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle multiple task file variations', () => {
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
# tasks.json
# tasks/
tasks/
#tasks.json
# More content
more.txt`;
const templateContent = `# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
manageGitignoreFile(testGitignorePath, templateContent, true);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain non-task content
expect(content).toContain('# Existing');
expect(content).toContain('existing.txt');
expect(content).toContain('# More content');
expect(content).toContain('more.txt');
// Should have clean task section with preference applied (storeTasksInGit = true means commented)
expect(content).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
// Should not have multiple task sections
const taskFileMatches = content.match(/# Task files/g);
expect(taskFileMatches).toHaveLength(1);
});
});
describe('Error Handling', () => {
test('should handle permission errors gracefully', () => {
// Create a directory where we would create the file, then remove write permissions
const readOnlyDir = path.join(tempDir, 'readonly');
fs.mkdirSync(readOnlyDir);
fs.chmodSync(readOnlyDir, 0o444); // Read-only
const readOnlyGitignorePath = path.join(readOnlyDir, '.gitignore');
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/ `;
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
expect(() => {
manageGitignoreFile(
readOnlyGitignorePath,
templateContent,
false,
mockLog
);
}).toThrow();
// Verify error was logged
expect(logs).toContainEqual({
level: 'error',
message: expect.stringContaining('Failed to create')
});
// Restore permissions for cleanup
fs.chmodSync(readOnlyDir, 0o755);
});
test('should handle read errors on existing files', () => {
// Create a file then remove read permissions
fs.writeFileSync(testGitignorePath, 'existing content');
fs.chmodSync(testGitignorePath, 0o000); // No permissions
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/ `;
const logs = [];
const mockLog = (level, message) => logs.push({ level, message });
expect(() => {
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
}).toThrow();
// Verify error was logged
expect(logs).toContainEqual({
level: 'error',
message: expect.stringContaining('Failed to merge content')
});
// Restore permissions for cleanup
fs.chmodSync(testGitignorePath, 0o644);
});
});
describe('Real-world Scenarios', () => {
test('should handle typical Node.js project .gitignore', () => {
const existingNodeGitignore = `# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next`;
const taskMasterTemplate = `# Logs
logs
*.log
# Dependencies
node_modules/
# Environment variables
.env
# Build output
dist/
build/
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingNodeGitignore);
manageGitignoreFile(testGitignorePath, taskMasterTemplate, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing Node.js specific entries
expect(content).toContain('npm-debug.log*');
expect(content).toContain('yarn-debug.log*');
expect(content).toContain('*.pid');
expect(content).toContain('jspm_packages/');
expect(content).toContain('.npm');
expect(content).toContain('*.tgz');
expect(content).toContain('.yarn-integrity');
expect(content).toContain('.next');
// Should add new content from template that wasn't present
expect(content).toContain('dist/');
expect(content).toContain('build/');
// Should add task files section with correct preference (storeTasksInGit = false means uncommented)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
// Should not duplicate common entries
const nodeModulesMatches = content.match(/node_modules\//g);
expect(nodeModulesMatches).toHaveLength(1);
const logsMatches = content.match(/# Logs/g);
expect(logsMatches).toHaveLength(1);
});
test('should handle project with existing task files in git', () => {
const existingContent = `# Dependencies
node_modules/
# Logs
*.log
# Current task setup - keeping in git
# Task files
tasks.json
tasks/
# Build output
dist/`;
const templateContent = `# New template
# Dependencies
node_modules/
# Task files
tasks.json
tasks/ `;
fs.writeFileSync(testGitignorePath, existingContent);
// Change preference to exclude tasks from git (storeTasksInGit = false means uncommented/ignored)
manageGitignoreFile(testGitignorePath, templateContent, false);
const content = fs.readFileSync(testGitignorePath, 'utf8');
// Should retain existing content
expect(content).toContain('# Dependencies');
expect(content).toContain('node_modules/');
expect(content).toContain('# Logs');
expect(content).toContain('*.log');
expect(content).toContain('# Build output');
expect(content).toContain('dist/');
// Should update task preference to uncommented (storeTasksInGit = false)
expect(content).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
});
});

View File

@@ -0,0 +1,538 @@
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Reduce noise in test output
process.env.TASKMASTER_LOG_LEVEL = 'error';
// === Mock everything early ===
jest.mock('child_process', () => ({ execSync: jest.fn() }));
jest.mock('fs', () => ({
...jest.requireActual('fs'),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
readFileSync: jest.fn(),
appendFileSync: jest.fn(),
existsSync: jest.fn(),
mkdtempSync: jest.requireActual('fs').mkdtempSync,
rmSync: jest.requireActual('fs').rmSync
}));
// Mock console methods to suppress output
const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
consoleMethods.forEach((method) => {
global.console[method] = jest.fn();
});
// Mock ES modules using unstable_mockModule
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
isSilentMode: jest.fn(() => true),
enableSilentMode: jest.fn(),
log: jest.fn(),
findProjectRoot: jest.fn(() => process.cwd())
}));
// Mock git-utils module
jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
insideGitWorkTree: jest.fn(() => false)
}));
// Mock rule transformer
jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
convertAllRulesToProfileRules: jest.fn(),
getRulesProfile: jest.fn(() => ({
conversionConfig: {},
globalReplacements: []
}))
}));
// Mock any other modules that might output or do real operations
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
saveConfig: jest.fn()
}));
// Mock display libraries
jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
jest.mock('chalk', () => ({
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
red: jest.fn((text) => text),
yellow: jest.fn((text) => text),
cyan: jest.fn((text) => text),
white: jest.fn((text) => text),
dim: jest.fn((text) => text),
bold: jest.fn((text) => text),
underline: jest.fn((text) => text)
}));
const { execSync } = jest.requireMock('child_process');
const mockFs = jest.requireMock('fs');
// Import the mocked modules
const mockUtils = await import('../../scripts/modules/utils.js');
const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
// Import after mocks
const { initializeProject } = await import('../../scripts/init.js');
describe('initializeProject Git / Alias flag logic', () => {
let tmpDir;
const origCwd = process.cwd();
// Standard non-interactive options for all tests
const baseOptions = {
yes: true,
skipInstall: true,
name: 'test-project',
description: 'Test project description',
version: '1.0.0',
author: 'Test Author'
};
beforeEach(() => {
jest.clearAllMocks();
// Set up basic fs mocks
mockFs.mkdirSync.mockImplementation(() => {});
mockFs.writeFileSync.mockImplementation(() => {});
mockFs.readFileSync.mockImplementation((filePath) => {
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return 'mock template content';
}
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return '# existing config';
}
return '';
});
mockFs.appendFileSync.mockImplementation(() => {});
mockFs.existsSync.mockImplementation((filePath) => {
// Template source files exist
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return true;
}
// Shell config files exist by default
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return true;
}
return false;
});
// Reset utils mocks
mockUtils.isSilentMode.mockReturnValue(true);
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
// Default execSync mock
execSync.mockImplementation(() => '');
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
process.chdir(tmpDir);
});
afterEach(() => {
process.chdir(origCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('Git Flag Behavior', () => {
it('completes successfully with git:false in dry run', async () => {
const result = await initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
it('completes successfully with git:true when not inside repo', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('completes successfully when already inside repo', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(true);
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('uses default git behavior without errors', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
await expect(
initializeProject({
...baseOptions,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles git command failures gracefully', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
execSync.mockImplementation((cmd) => {
if (cmd.includes('git init')) {
throw new Error('git not found');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Alias Flag Behavior', () => {
it('completes successfully when aliases:true and environment is set up', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
it('completes successfully when aliases:false', async () => {
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles missing shell gracefully', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
delete process.env.SHELL; // Remove shell env var
process.env.HOME = '/mock/home';
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
it('handles missing shell config file gracefully', async () => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
// Shell config doesn't exist
mockFs.existsSync.mockImplementation((filePath) => {
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
return false;
}
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
return true;
}
return false;
});
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: true,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
});
describe('Flag Combinations', () => {
it.each`
git | aliases | description
${true} | ${true} | ${'git & aliases enabled'}
${true} | ${false} | ${'git enabled, aliases disabled'}
${false} | ${true} | ${'git disabled, aliases enabled'}
${false} | ${false} | ${'git & aliases disabled'}
`('handles $description without errors', async ({ git, aliases }) => {
const originalShell = process.env.SHELL;
const originalHome = process.env.HOME;
if (aliases) {
process.env.SHELL = '/bin/zsh';
process.env.HOME = '/mock/home';
}
if (git) {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
}
await expect(
initializeProject({
...baseOptions,
git,
aliases,
dryRun: false
})
).resolves.not.toThrow();
process.env.SHELL = originalShell;
process.env.HOME = originalHome;
});
});
describe('Dry Run Mode', () => {
it('returns dry run result and performs no operations', async () => {
const result = await initializeProject({
...baseOptions,
git: true,
aliases: true,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
it.each`
git | aliases | description
${true} | ${false} | ${'git-specific behavior'}
${false} | ${false} | ${'no-git behavior'}
${false} | ${true} | ${'alias behavior'}
`('shows $description in dry run', async ({ git, aliases }) => {
const result = await initializeProject({
...baseOptions,
git,
aliases,
dryRun: true
});
expect(result.dryRun).toBe(true);
});
});
describe('Error Handling', () => {
it('handles npm install failures gracefully', async () => {
execSync.mockImplementation((cmd) => {
if (cmd.includes('npm install')) {
throw new Error('npm failed');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
skipInstall: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles git failures gracefully', async () => {
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
execSync.mockImplementation((cmd) => {
if (cmd.includes('git init')) {
throw new Error('git failed');
}
return '';
});
await expect(
initializeProject({
...baseOptions,
git: true,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles file system errors gracefully', async () => {
mockFs.mkdirSync.mockImplementation(() => {
throw new Error('Permission denied');
});
// Should handle file system errors gracefully
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Non-Interactive Mode', () => {
it('bypasses prompts with yes:true', async () => {
const result = await initializeProject({
...baseOptions,
git: true,
aliases: true,
dryRun: true
});
expect(result).toEqual({ dryRun: true });
});
it('completes without hanging', async () => {
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('handles all flag combinations without hanging', async () => {
const flagCombinations = [
{ git: true, aliases: true },
{ git: true, aliases: false },
{ git: false, aliases: true },
{ git: false, aliases: false },
{} // No flags (uses defaults)
];
for (const flags of flagCombinations) {
await expect(
initializeProject({
...baseOptions,
...flags,
dryRun: true // Use dry run for speed
})
).resolves.not.toThrow();
}
});
it('accepts complete project details', async () => {
await expect(
initializeProject({
name: 'test-project',
description: 'test description',
version: '2.0.0',
author: 'Test User',
git: false,
aliases: false,
dryRun: true
})
).resolves.not.toThrow();
});
it('works with skipInstall option', async () => {
await expect(
initializeProject({
...baseOptions,
skipInstall: true,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
});
describe('Function Integration', () => {
it('calls utility functions without errors', async () => {
await initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
});
// Verify that utility functions were called
expect(mockUtils.isSilentMode).toHaveBeenCalled();
expect(
mockRuleTransformer.convertAllRulesToProfileRules
).toHaveBeenCalled();
});
it('handles template operations gracefully', async () => {
// Make file operations throw errors
mockFs.writeFileSync.mockImplementation(() => {
throw new Error('Write failed');
});
// Should complete despite file operation failures
await expect(
initializeProject({
...baseOptions,
git: false,
aliases: false,
dryRun: false
})
).resolves.not.toThrow();
});
it('validates boolean flag conversion', async () => {
// Test the boolean flag handling specifically
await expect(
initializeProject({
...baseOptions,
git: true, // Should convert to initGit: true
aliases: false, // Should convert to addAliases: false
dryRun: true
})
).resolves.not.toThrow();
await expect(
initializeProject({
...baseOptions,
git: false, // Should convert to initGit: false
aliases: true, // Should convert to addAliases: true
dryRun: true
})
).resolves.not.toThrow();
});
});
});

View File

@@ -0,0 +1,439 @@
/**
* Unit tests for manage-gitignore.js module
* Tests the logic with Jest spies instead of mocked modules
*/
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Import the module under test and its exports
import manageGitignoreFile, {
normalizeLine,
isTaskLine,
buildTaskFilesSection,
TASK_FILES_COMMENT,
TASK_JSON_PATTERN,
TASK_DIR_PATTERN
} from '../../src/utils/manage-gitignore.js';
describe('manage-gitignore.js Unit Tests', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manage-gitignore-test-'));
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
});
describe('Constants', () => {
test('should have correct constant values', () => {
expect(TASK_FILES_COMMENT).toBe('# Task files');
expect(TASK_JSON_PATTERN).toBe('tasks.json');
expect(TASK_DIR_PATTERN).toBe('tasks/');
});
});
describe('normalizeLine function', () => {
test('should remove leading/trailing whitespace', () => {
expect(normalizeLine(' test ')).toBe('test');
});
test('should remove comment hash and trim', () => {
expect(normalizeLine('# tasks.json')).toBe('tasks.json');
expect(normalizeLine('#tasks/')).toBe('tasks/');
});
test('should handle empty strings', () => {
expect(normalizeLine('')).toBe('');
expect(normalizeLine(' ')).toBe('');
});
test('should handle lines without comments', () => {
expect(normalizeLine('tasks.json')).toBe('tasks.json');
});
});
describe('isTaskLine function', () => {
test('should identify task.json patterns', () => {
expect(isTaskLine('tasks.json')).toBe(true);
expect(isTaskLine('# tasks.json')).toBe(true);
expect(isTaskLine(' # tasks.json ')).toBe(true);
});
test('should identify tasks/ patterns', () => {
expect(isTaskLine('tasks/')).toBe(true);
expect(isTaskLine('# tasks/')).toBe(true);
expect(isTaskLine(' # tasks/ ')).toBe(true);
});
test('should reject non-task patterns', () => {
expect(isTaskLine('node_modules/')).toBe(false);
expect(isTaskLine('# Some comment')).toBe(false);
expect(isTaskLine('')).toBe(false);
expect(isTaskLine('tasks.txt')).toBe(false);
});
});
describe('buildTaskFilesSection function', () => {
test('should build commented section when storeTasksInGit is true (tasks stored in git)', () => {
const result = buildTaskFilesSection(true);
expect(result).toEqual(['# Task files', '# tasks.json', '# tasks/ ']);
});
test('should build uncommented section when storeTasksInGit is false (tasks ignored)', () => {
const result = buildTaskFilesSection(false);
expect(result).toEqual(['# Task files', 'tasks.json', 'tasks/ ']);
});
});
describe('manageGitignoreFile function - Input Validation', () => {
test('should throw error for invalid targetPath', () => {
expect(() => {
manageGitignoreFile('', 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile(null, 'content', false);
}).toThrow('targetPath must be a non-empty string');
expect(() => {
manageGitignoreFile('invalid.txt', 'content', false);
}).toThrow('targetPath must end with .gitignore');
});
test('should throw error for invalid content', () => {
expect(() => {
manageGitignoreFile('.gitignore', '', false);
}).toThrow('content must be a non-empty string');
expect(() => {
manageGitignoreFile('.gitignore', null, false);
}).toThrow('content must be a non-empty string');
});
test('should throw error for invalid storeTasksInGit', () => {
expect(() => {
manageGitignoreFile('.gitignore', 'content', 'not-boolean');
}).toThrow('storeTasksInGit must be a boolean');
});
});
describe('manageGitignoreFile function - File Operations with Spies', () => {
let writeFileSyncSpy;
let readFileSyncSpy;
let existsSyncSpy;
let mockLog;
beforeEach(() => {
// Set up spies
writeFileSyncSpy = jest
.spyOn(fs, 'writeFileSync')
.mockImplementation(() => {});
readFileSyncSpy = jest
.spyOn(fs, 'readFileSync')
.mockImplementation(() => '');
existsSyncSpy = jest
.spyOn(fs, 'existsSync')
.mockImplementation(() => false);
mockLog = jest.fn();
});
afterEach(() => {
// Restore original implementations
writeFileSyncSpy.mockRestore();
readFileSyncSpy.mockRestore();
existsSyncSpy.mockRestore();
});
describe('New File Creation', () => {
const templateContent = `# Logs
logs
*.log
# Task files
tasks.json
tasks/ `;
test('should create new file with commented task lines when storeTasksInGit is true', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
# tasks.json
# tasks/ `
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should create new file with uncommented task lines when storeTasksInGit is false', () => {
existsSyncSpy.mockReturnValue(false); // File doesn't exist
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
`# Logs
logs
*.log
# Task files
tasks.json
tasks/ `
);
expect(mockLog).toHaveBeenCalledWith(
'success',
'Created .gitignore with full template'
);
});
test('should handle write errors gracefully', () => {
existsSyncSpy.mockReturnValue(false);
const writeError = new Error('Permission denied');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Permission denied');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to create .gitignore: Permission denied'
);
});
});
describe('File Merging', () => {
const templateContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
tasks.json
tasks/ `;
test('should merge with existing file and add new content', () => {
const existingContent = `# Old content
old-file.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true); // File exists
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Old content')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Logs')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Dependencies')
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(
'.gitignore',
expect.stringContaining('# Task files')
);
});
test('should remove existing task section and replace with new preferences', () => {
const existingContent = `# Existing
existing.txt
# Task files
tasks.json
tasks/
# More content
more.txt`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should contain existing non-task content
expect(writtenContent).toContain('# Existing');
expect(writtenContent).toContain('existing.txt');
expect(writtenContent).toContain('# More content');
expect(writtenContent).toContain('more.txt');
// Should contain new template content
expect(writtenContent).toContain('# Logs');
expect(writtenContent).toContain('# Dependencies');
// Should have uncommented task lines (storeTasksInGit = false means ignore tasks)
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
);
});
test('should handle different task preferences correctly', () => {
const existingContent = `# Existing
existing.txt
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
// Test with storeTasksInGit = true (commented)
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toMatch(
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
);
});
test('should not duplicate existing template content', () => {
const existingContent = `# Logs
logs
*.log
# Dependencies
node_modules/
# Task files
# tasks.json
# tasks/`;
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue(existingContent);
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
// Should not duplicate the logs section
const logsCount = (writtenContent.match(/# Logs/g) || []).length;
expect(logsCount).toBe(1);
// Should not duplicate dependencies
const depsCount = (writtenContent.match(/# Dependencies/g) || [])
.length;
expect(depsCount).toBe(1);
});
test('should handle read errors gracefully', () => {
existsSyncSpy.mockReturnValue(true);
const readError = new Error('File not readable');
readFileSyncSpy.mockImplementation(() => {
throw readError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('File not readable');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: File not readable'
);
});
test('should handle write errors during merge gracefully', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('existing content');
const writeError = new Error('Disk full');
writeFileSyncSpy.mockImplementation(() => {
throw writeError;
});
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
}).toThrow('Disk full');
expect(mockLog).toHaveBeenCalledWith(
'error',
'Failed to merge content with .gitignore: Disk full'
);
});
});
describe('Edge Cases', () => {
test('should work without log function', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Test
test.txt
# Task files
tasks.json
tasks/`;
expect(() => {
manageGitignoreFile('.gitignore', templateContent, false);
}).not.toThrow();
expect(writeFileSyncSpy).toHaveBeenCalled();
});
test('should handle empty existing file', () => {
existsSyncSpy.mockReturnValue(true);
readFileSyncSpy.mockReturnValue('');
const templateContent = `# Task files
tasks.json
tasks/`;
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
expect(writeFileSyncSpy).toHaveBeenCalled();
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toContain('# Task files');
});
test('should handle template with only task files', () => {
existsSyncSpy.mockReturnValue(false);
const templateContent = `# Task files
tasks.json
tasks/ `;
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
expect(writtenContent).toBe(`# Task files
# tasks.json
# tasks/ `);
});
});
});
});

72
vite.config.js Normal file
View File

@@ -0,0 +1,72 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default defineConfig({
build: {
ssr: true, // Use SSR mode for Node.js
rollupOptions: {
// Multiple entry points for different applications
input: {
'task-master': resolve(__dirname, 'bin/task-master.js'), // CLI tool
'task-master-mcp': resolve(__dirname, 'mcp-server/server.js') // MCP server
},
// Bundle everything except Node.js built-ins
external: [
// Node.js built-in modules
'fs',
'fs/promises',
'path',
'os',
'crypto',
'http',
'https',
'net',
'tls',
'child_process',
'util',
'events',
'stream',
'url',
'querystring',
'buffer',
'module',
'worker_threads',
'readline',
'process',
'assert',
'zlib',
'dns',
'perf_hooks',
// Optional dependencies that might not be available
'@anthropic-ai/claude-code'
],
output: {
// Generate separate files for each entry
dir: 'dist',
format: 'cjs', // CommonJS for Node.js compatibility
entryFileNames: '[name].cjs',
chunkFileNames: 'chunks/[name]-[hash].cjs',
assetFileNames: 'assets/[name].[ext]'
},
plugins: [
nodeResolve({
preferBuiltins: true,
exportConditions: ['node']
})
]
},
target: 'node18',
outDir: 'dist',
minify: false, // Keep readable for debugging
sourcemap: false
},
define: {
// Define any environment variables if needed
'process.env.NODE_ENV': '"production"'
},
ssr: {
// Don't externalize any dependencies - bundle them all
noExternal: true
}
});