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
|
||||
|
||||
## 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# docs
|
||||
|
||||
## 0.0.6
|
||||
|
||||
## 0.0.5
|
||||
|
||||
## 0.0.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Change Log
|
||||
|
||||
## 0.25.6
|
||||
|
||||
## 0.25.6-rc.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.6-rc.0",
|
||||
"version": "0.25.6",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.29.0-rc.1",
|
||||
"version": "0.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.29.0-rc.1",
|
||||
"version": "0.29.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -125,13 +125,13 @@
|
||||
}
|
||||
},
|
||||
"apps/docs": {
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.6",
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.2.111"
|
||||
}
|
||||
},
|
||||
"apps/extension": {
|
||||
"version": "0.25.6-rc.0",
|
||||
"version": "0.25.6",
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
@@ -27136,7 +27136,7 @@
|
||||
},
|
||||
"packages/claude-code-plugin": {
|
||||
"name": "@tm/claude-code-plugin",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "MIT WITH Commons-Clause"
|
||||
},
|
||||
"packages/tm-core": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# @tm/ai-sdk-provider-grok-cli
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
@@ -4,4 +4,6 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 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",
|
||||
"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",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 0.26.1
|
||||
|
||||
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
|
||||
* @param {string} startDir - Directory to start searching from
|
||||
* @returns {string|null} - Project root path or null if not found
|
||||
* Traverses upwards from startDir until a project marker is found or filesystem root is reached
|
||||
* 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()) {
|
||||
// Define project markers that indicate a project root
|
||||
// Prioritize Task Master specific markers first
|
||||
const projectMarkers = [
|
||||
'.taskmaster',
|
||||
TASKMASTER_TASKS_FILE,
|
||||
'tasks.json',
|
||||
LEGACY_TASKS_FILE,
|
||||
'.git',
|
||||
'.svn',
|
||||
'package.json',
|
||||
'yarn.lock',
|
||||
'package-lock.json',
|
||||
'pnpm-lock.yaml'
|
||||
'.taskmaster', // Task Master directory (highest priority)
|
||||
TASKMASTER_CONFIG_FILE, // .taskmaster/config.json
|
||||
TASKMASTER_TASKS_FILE, // .taskmaster/tasks/tasks.json
|
||||
LEGACY_CONFIG_FILE, // .taskmasterconfig (legacy)
|
||||
LEGACY_TASKS_FILE, // tasks/tasks.json (legacy)
|
||||
'tasks.json', // Root tasks.json (legacy)
|
||||
'.git', // Git repository
|
||||
'.svn', // SVN repository
|
||||
'package.json', // Node.js project
|
||||
'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);
|
||||
@@ -69,19 +81,36 @@ export function findProjectRoot(startDir = process.cwd()) {
|
||||
const maxDepth = 50; // Reasonable limit to prevent infinite loops
|
||||
let depth = 0;
|
||||
|
||||
// Traverse upwards looking for project markers
|
||||
while (currentDir !== rootDir && depth < maxDepth) {
|
||||
// Check if current directory contains any project markers
|
||||
for (const marker of projectMarkers) {
|
||||
const markerPath = path.join(currentDir, marker);
|
||||
try {
|
||||
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++;
|
||||
}
|
||||
|
||||
// Fallback to current working directory if no project root found
|
||||
// This ensures the function always returns a valid path
|
||||
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