fix: enhance findProjectRoot to traverse parent directories (#1302)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add changelog highlights to auto-update notifications
|
|
||||||
|
|
||||||
When the CLI auto-updates to a new version, it now displays a "What's New" section.
|
|
||||||
7
.changeset/fix-parent-directory-traversal.md
Normal file
7
.changeset/fix-parent-directory-traversal.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Enable Task Master commands to traverse parent directories to find project root from nested paths
|
||||||
|
|
||||||
|
Fixes #1301
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add Claude Code plugin with marketplace distribution
|
|
||||||
|
|
||||||
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
|
|
||||||
|
|
||||||
## 🎉 New: Claude Code Plugin
|
|
||||||
|
|
||||||
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
|
|
||||||
|
|
||||||
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
|
|
||||||
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
|
|
||||||
- **MCP server integration** for deep Claude Code integration
|
|
||||||
|
|
||||||
**Installation:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/plugin marketplace add eyaltoledano/claude-task-master
|
|
||||||
/plugin install taskmaster@taskmaster
|
|
||||||
```
|
|
||||||
|
|
||||||
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
|
|
||||||
|
|
||||||
- Shows plugin installation instructions
|
|
||||||
- Only manages CLAUDE.md imports for agent instructions
|
|
||||||
- Directs users to install the official plugin
|
|
||||||
|
|
||||||
**Migration for Existing Users:**
|
|
||||||
|
|
||||||
If you previously used `rules add claude`:
|
|
||||||
|
|
||||||
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
|
|
||||||
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
|
|
||||||
3. remove old `.claude/commands/` and `.claude/agents/` directories
|
|
||||||
|
|
||||||
**Why This Change?**
|
|
||||||
|
|
||||||
Claude Code plugins provide:
|
|
||||||
|
|
||||||
- ✅ Automatic updates when we release new features
|
|
||||||
- ✅ Better command organization and naming
|
|
||||||
- ✅ Seamless integration with Claude Code
|
|
||||||
- ✅ No manual file copying or management
|
|
||||||
|
|
||||||
The plugin system is the future of Task Master AI integration with Claude Code!
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
|
|
||||||
|
|
||||||
Key features:
|
|
||||||
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
|
|
||||||
- Inline instructions at decision points guide AI through each section
|
|
||||||
- Good/bad examples for immediate pattern matching
|
|
||||||
- Flexible plain-text format with XML-style tags for parseability
|
|
||||||
- Critical dependency-graph section ensures correct task ordering
|
|
||||||
- Automatic inclusion during `task-master init`
|
|
||||||
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
|
|
||||||
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
|
|
||||||
|
|
||||||
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix cross-level task dependencies not being saved
|
|
||||||
|
|
||||||
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"mode": "exit",
|
|
||||||
"tag": "rc",
|
|
||||||
"initialVersions": {
|
|
||||||
"task-master-ai": "0.29.0-rc.0",
|
|
||||||
"@tm/cli": "",
|
|
||||||
"docs": "0.0.5",
|
|
||||||
"extension": "0.25.6-rc.0",
|
|
||||||
"@tm/ai-sdk-provider-grok-cli": "",
|
|
||||||
"@tm/build-config": "",
|
|
||||||
"@tm/claude-code-plugin": "0.0.1",
|
|
||||||
"@tm/core": ""
|
|
||||||
},
|
|
||||||
"changesets": [
|
|
||||||
"auto-update-changelog-highlights",
|
|
||||||
"mean-planes-wave",
|
|
||||||
"nice-ways-hope",
|
|
||||||
"plain-falcons-serve",
|
|
||||||
"silent-bushes-grow",
|
|
||||||
"smart-owls-relax"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Improve refresh token when authenticating
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
|
|
||||||
|
|
||||||
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
|
|
||||||
|
|
||||||
Key improvements:
|
|
||||||
- Automatic integration with complexity analysis reports
|
|
||||||
- Tag-aware complexity report path resolution
|
|
||||||
- Intelligent subtask count determination based on task complexity
|
|
||||||
- Falls back to defaults when complexity analysis is unavailable
|
|
||||||
- Enhanced logging for better visibility into expansion decisions
|
|
||||||
|
|
||||||
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
|
|
||||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,5 +1,88 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
|
## 0.29.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#1286](https://github.com/eyaltoledano/claude-task-master/pull/1286) [`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add changelog highlights to auto-update notifications
|
||||||
|
|
||||||
|
When the CLI auto-updates to a new version, it now displays a "What's New" section.
|
||||||
|
|
||||||
|
- [#1293](https://github.com/eyaltoledano/claude-task-master/pull/1293) [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code plugin with marketplace distribution
|
||||||
|
|
||||||
|
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
|
||||||
|
|
||||||
|
## 🎉 New: Claude Code Plugin
|
||||||
|
|
||||||
|
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
|
||||||
|
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
|
||||||
|
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
|
||||||
|
- **MCP server integration** for deep Claude Code integration
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/plugin marketplace add eyaltoledano/claude-task-master
|
||||||
|
/plugin install taskmaster@taskmaster
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
|
||||||
|
- Shows plugin installation instructions
|
||||||
|
- Only manages CLAUDE.md imports for agent instructions
|
||||||
|
- Directs users to install the official plugin
|
||||||
|
|
||||||
|
**Migration for Existing Users:**
|
||||||
|
|
||||||
|
If you previously used `rules add claude`:
|
||||||
|
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
|
||||||
|
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
|
||||||
|
3. remove old `.claude/commands/` and `.claude/agents/` directories
|
||||||
|
|
||||||
|
**Why This Change?**
|
||||||
|
|
||||||
|
Claude Code plugins provide:
|
||||||
|
- ✅ Automatic updates when we release new features
|
||||||
|
- ✅ Better command organization and naming
|
||||||
|
- ✅ Seamless integration with Claude Code
|
||||||
|
- ✅ No manual file copying or management
|
||||||
|
|
||||||
|
The plugin system is the future of Task Master AI integration with Claude Code!
|
||||||
|
|
||||||
|
- [#1285](https://github.com/eyaltoledano/claude-task-master/pull/1285) [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
|
||||||
|
- Inline instructions at decision points guide AI through each section
|
||||||
|
- Good/bad examples for immediate pattern matching
|
||||||
|
- Flexible plain-text format with XML-style tags for parseability
|
||||||
|
- Critical dependency-graph section ensures correct task ordering
|
||||||
|
- Automatic inclusion during `task-master init`
|
||||||
|
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
|
||||||
|
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
|
||||||
|
|
||||||
|
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
|
||||||
|
|
||||||
|
- [#1287](https://github.com/eyaltoledano/claude-task-master/pull/1287) [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
|
||||||
|
|
||||||
|
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
|
||||||
|
|
||||||
|
Key improvements:
|
||||||
|
- Automatic integration with complexity analysis reports
|
||||||
|
- Tag-aware complexity report path resolution
|
||||||
|
- Intelligent subtask count determination based on task complexity
|
||||||
|
- Falls back to defaults when complexity analysis is unavailable
|
||||||
|
- Enhanced logging for better visibility into expansion decisions
|
||||||
|
|
||||||
|
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#1191](https://github.com/eyaltoledano/claude-task-master/pull/1191) [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix cross-level task dependencies not being saved
|
||||||
|
|
||||||
|
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
|
||||||
|
|
||||||
|
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`4c1ef2c`](https://github.com/eyaltoledano/claude-task-master/commit/4c1ef2ca94411c53bcd2a78ec710b06c500236dd) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
|
||||||
|
|
||||||
## 0.29.0-rc.1
|
## 0.29.0-rc.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @tm/core@null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
- Updated dependencies []:
|
- Updated dependencies []:
|
||||||
- @tm/core@null
|
- @tm/core@null
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# docs
|
# docs
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
## 0.0.4
|
## 0.0.4
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Task Master documentation powered by Mintlify",
|
"description": "Task Master documentation powered by Mintlify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 0.25.6
|
||||||
|
|
||||||
## 0.25.6-rc.0
|
## 0.25.6-rc.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.6-rc.0",
|
"version": "0.25.6",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.1",
|
"version": "0.29.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.1",
|
"version": "0.29.0",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -125,13 +125,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/docs": {
|
"apps/docs": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.2.111"
|
"mintlify": "^4.2.111"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.6-rc.0",
|
"version": "0.25.6",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
@@ -27136,7 +27136,7 @@
|
|||||||
},
|
},
|
||||||
"packages/claude-code-plugin": {
|
"packages/claude-code-plugin": {
|
||||||
"name": "@tm/claude-code-plugin",
|
"name": "@tm/claude-code-plugin",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"license": "MIT WITH Commons-Clause"
|
"license": "MIT WITH Commons-Clause"
|
||||||
},
|
},
|
||||||
"packages/tm-core": {
|
"packages/tm-core": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0-rc.1",
|
"version": "0.29.0",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# @tm/ai-sdk-provider-grok-cli
|
# @tm/ai-sdk-provider-grok-cli
|
||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|||||||
@@ -4,4 +4,6 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|||||||
3
packages/claude-code-plugin/CHANGELOG.md
Normal file
3
packages/claude-code-plugin/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# @tm/claude-code-plugin
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/claude-code-plugin",
|
"name": "@tm/claude-code-plugin",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
|
## null
|
||||||
|
|
||||||
## 0.26.1
|
## 0.26.1
|
||||||
|
|
||||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||||
|
|||||||
@@ -47,21 +47,33 @@ export function normalizeProjectRoot(projectRoot) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the project root directory by looking for project markers
|
* Find the project root directory by looking for project markers
|
||||||
* @param {string} startDir - Directory to start searching from
|
* Traverses upwards from startDir until a project marker is found or filesystem root is reached
|
||||||
* @returns {string|null} - Project root path or null if not found
|
* Limited to 50 parent directory levels to prevent excessive traversal
|
||||||
|
* @param {string} startDir - Directory to start searching from (defaults to process.cwd())
|
||||||
|
* @returns {string} - Project root path (falls back to current directory if no markers found)
|
||||||
*/
|
*/
|
||||||
export function findProjectRoot(startDir = process.cwd()) {
|
export function findProjectRoot(startDir = process.cwd()) {
|
||||||
|
// Define project markers that indicate a project root
|
||||||
|
// Prioritize Task Master specific markers first
|
||||||
const projectMarkers = [
|
const projectMarkers = [
|
||||||
'.taskmaster',
|
'.taskmaster', // Task Master directory (highest priority)
|
||||||
TASKMASTER_TASKS_FILE,
|
TASKMASTER_CONFIG_FILE, // .taskmaster/config.json
|
||||||
'tasks.json',
|
TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json
|
||||||
LEGACY_TASKS_FILE,
|
LEGACY_CONFIG_FILE, // .taskmasterconfig (legacy)
|
||||||
'.git',
|
LEGACY_TASKS_FILE, // tasks/tasks.json (legacy)
|
||||||
'.svn',
|
'tasks.json', // Root tasks.json (legacy)
|
||||||
'package.json',
|
'.git', // Git repository
|
||||||
'yarn.lock',
|
'.svn', // SVN repository
|
||||||
'package-lock.json',
|
'package.json', // Node.js project
|
||||||
'pnpm-lock.yaml'
|
'yarn.lock', // Yarn project
|
||||||
|
'package-lock.json', // npm project
|
||||||
|
'pnpm-lock.yaml', // pnpm project
|
||||||
|
'Cargo.toml', // Rust project
|
||||||
|
'go.mod', // Go project
|
||||||
|
'pyproject.toml', // Python project
|
||||||
|
'requirements.txt', // Python project
|
||||||
|
'Gemfile', // Ruby project
|
||||||
|
'composer.json' // PHP project
|
||||||
];
|
];
|
||||||
|
|
||||||
let currentDir = path.resolve(startDir);
|
let currentDir = path.resolve(startDir);
|
||||||
@@ -69,19 +81,36 @@ export function findProjectRoot(startDir = process.cwd()) {
|
|||||||
const maxDepth = 50; // Reasonable limit to prevent infinite loops
|
const maxDepth = 50; // Reasonable limit to prevent infinite loops
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
|
|
||||||
|
// Traverse upwards looking for project markers
|
||||||
while (currentDir !== rootDir && depth < maxDepth) {
|
while (currentDir !== rootDir && depth < maxDepth) {
|
||||||
// Check if current directory contains any project markers
|
// Check if current directory contains any project markers
|
||||||
for (const marker of projectMarkers) {
|
for (const marker of projectMarkers) {
|
||||||
const markerPath = path.join(currentDir, marker);
|
const markerPath = path.join(currentDir, marker);
|
||||||
if (fs.existsSync(markerPath)) {
|
try {
|
||||||
return currentDir;
|
if (fs.existsSync(markerPath)) {
|
||||||
|
// Found a project marker - return this directory as project root
|
||||||
|
return currentDir;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore permission errors and continue searching
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentDir = path.dirname(currentDir);
|
|
||||||
|
// Move up one directory level
|
||||||
|
const parentDir = path.dirname(currentDir);
|
||||||
|
|
||||||
|
// Safety check: if dirname returns the same path, we've hit the root
|
||||||
|
if (parentDir === currentDir) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDir = parentDir;
|
||||||
depth++;
|
depth++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to current working directory if no project root found
|
// Fallback to current working directory if no project root found
|
||||||
|
// This ensures the function always returns a valid path
|
||||||
return process.cwd();
|
return process.cwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
223
tests/unit/path-utils-find-project-root.test.js
Normal file
223
tests/unit/path-utils-find-project-root.test.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for findProjectRoot() function
|
||||||
|
* Tests the parent directory traversal functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// Import the function to test
|
||||||
|
import { findProjectRoot } from '../../src/utils/path-utils.js';
|
||||||
|
|
||||||
|
describe('findProjectRoot', () => {
|
||||||
|
describe('Parent Directory Traversal', () => {
|
||||||
|
test('should find .taskmaster in parent directory', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
// .taskmaster exists only at /project
|
||||||
|
return normalized === path.normalize('/project/.taskmaster');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find .git in parent directory', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
return normalized === path.normalize('/project/.git');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find package.json in parent directory', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
return normalized === path.normalize('/project/package.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should traverse multiple levels to find project root', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
// Only exists at /project, not in any subdirectories
|
||||||
|
return normalized === path.normalize('/project/.taskmaster');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir/deep/nested');
|
||||||
|
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return current directory as fallback when no markers found', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
// No project markers exist anywhere
|
||||||
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = findProjectRoot('/some/random/path');
|
||||||
|
|
||||||
|
// Should fall back to process.cwd()
|
||||||
|
expect(result).toBe(process.cwd());
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should find markers at current directory before checking parent', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
// .git exists at /project/subdir, .taskmaster exists at /project
|
||||||
|
if (normalized.includes('/project/subdir/.git')) return true;
|
||||||
|
if (normalized.includes('/project/.taskmaster')) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
// Should find /project/subdir first because .git exists there,
|
||||||
|
// even though .taskmaster is earlier in the marker array
|
||||||
|
expect(result).toBe('/project/subdir');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle permission errors gracefully', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
// Throw permission error for checks in /project/subdir
|
||||||
|
if (normalized.startsWith('/project/subdir/')) {
|
||||||
|
throw new Error('EACCES: permission denied');
|
||||||
|
}
|
||||||
|
// Return true only for .taskmaster at /project
|
||||||
|
return normalized.includes('/project/.taskmaster');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
// Should handle permission errors in subdirectory and traverse to parent
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect filesystem root correctly', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
// No markers exist
|
||||||
|
mockExistsSync.mockReturnValue(false);
|
||||||
|
|
||||||
|
const result = findProjectRoot('/');
|
||||||
|
|
||||||
|
// Should stop at root and fall back to process.cwd()
|
||||||
|
expect(result).toBe(process.cwd());
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recognize various project markers', () => {
|
||||||
|
const projectMarkers = [
|
||||||
|
'.taskmaster',
|
||||||
|
'.git',
|
||||||
|
'package.json',
|
||||||
|
'Cargo.toml',
|
||||||
|
'go.mod',
|
||||||
|
'pyproject.toml',
|
||||||
|
'requirements.txt',
|
||||||
|
'Gemfile',
|
||||||
|
'composer.json'
|
||||||
|
];
|
||||||
|
|
||||||
|
projectMarkers.forEach((marker) => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
const normalized = path.normalize(checkPath);
|
||||||
|
return normalized.includes(`/project/${marker}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('/project/subdir');
|
||||||
|
|
||||||
|
expect(result).toBe('/project');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
test('should handle empty string as startDir', () => {
|
||||||
|
const result = findProjectRoot('');
|
||||||
|
|
||||||
|
// Should use process.cwd() or fall back appropriately
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle relative paths', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
mockExistsSync.mockImplementation((checkPath) => {
|
||||||
|
// Simulate .git existing in the resolved path
|
||||||
|
return checkPath.includes('.git');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = findProjectRoot('./subdir');
|
||||||
|
|
||||||
|
expect(typeof result).toBe('string');
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not exceed max depth limit', () => {
|
||||||
|
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||||
|
|
||||||
|
// Track how many times existsSync is called
|
||||||
|
let callCount = 0;
|
||||||
|
mockExistsSync.mockImplementation(() => {
|
||||||
|
callCount++;
|
||||||
|
return false; // Never find a marker
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a very deep path
|
||||||
|
const deepPath = '/a/'.repeat(100) + 'deep';
|
||||||
|
const result = findProjectRoot(deepPath);
|
||||||
|
|
||||||
|
// Should stop after max depth (50) and not check 100 levels
|
||||||
|
// Each level checks multiple markers, so callCount will be high but bounded
|
||||||
|
expect(callCount).toBeLessThan(1000); // Reasonable upper bound
|
||||||
|
// With 18 markers and max depth of 50, expect around 900 calls maximum
|
||||||
|
expect(callCount).toBeLessThanOrEqual(50 * 18);
|
||||||
|
|
||||||
|
mockExistsSync.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user