mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat(project-root): enhance project root detection with boundary markers (#1545)
This commit is contained in:
9
.changeset/lazy-lies-argue.md
Normal file
9
.changeset/lazy-lies-argue.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Smarter project root detection with boundary markers
|
||||
|
||||
- Prevents Task Master from incorrectly detecting `.taskmaster` folders in your home directory when working inside a different project
|
||||
- Now stops at project boundaries (`.git`, `package.json`, lock files) instead of searching all the way up to the filesystem root
|
||||
- Adds support for monorepo markers (`lerna.json`, `nx.json`, `turbo.json`) and additional lock files (`bun.lockb`, `deno.lock`)
|
||||
@@ -24,7 +24,8 @@
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"fastmcp": "^3.23.0",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^4.1.11",
|
||||
"dotenv": "^16.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Shared utilities for MCP tools
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
@@ -395,6 +396,13 @@ export function withToolContext<TArgs extends { projectRoot?: string }>(
|
||||
args: TArgs & { projectRoot: string },
|
||||
context: Context<undefined>
|
||||
) => {
|
||||
// Load project .env if it exists (won't overwrite MCP-provided env vars)
|
||||
// This ensures project-specific env vars are available to tool execution
|
||||
const envPath = path.join(args.projectRoot, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
dotenv.config({ path: envPath });
|
||||
}
|
||||
|
||||
// Create tmCore instance
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: args.projectRoot,
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.39.0",
|
||||
"version": "0.40.0-rc.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.39.0",
|
||||
"version": "0.40.0-rc.1",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -1850,7 +1850,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"task-master-ai": "*",
|
||||
"task-master-ai": "0.40.0-rc.1",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1971,6 +1971,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tm/core": "*",
|
||||
"dotenv": "^16.6.1",
|
||||
"fastmcp": "^3.23.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
|
||||
268
packages/tm-core/src/common/utils/project-root-finder.test.ts
Normal file
268
packages/tm-core/src/common/utils/project-root-finder.test.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* @fileoverview Integration tests for project root detection
|
||||
*
|
||||
* These tests verify real-world scenarios for project root detection,
|
||||
* particularly edge cases around:
|
||||
* - Empty directories (tm init scenario)
|
||||
* - .taskmaster in home/parent directories that should be ignored
|
||||
* - Monorepo detection
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { findProjectRoot } from './project-root-finder.js';
|
||||
|
||||
describe('findProjectRoot - Integration Tests', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary directory structure for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-integration-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temp directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Empty directory scenarios (tm init)', () => {
|
||||
it('should find .taskmaster in immediate parent (depth=1) even without boundary marker', () => {
|
||||
// Scenario: User is in a subdirectory of a project that only has .taskmaster
|
||||
// (no .git or package.json). This is a valid use case for projects where
|
||||
// user ran `tm init` but not `git init`.
|
||||
//
|
||||
// Structure:
|
||||
// /project/.taskmaster
|
||||
// /project/src/ (start here)
|
||||
//
|
||||
// NOTE: For `tm init` command specifically, the command should use
|
||||
// process.cwd() directly instead of findProjectRoot() to avoid
|
||||
// finding a stray .taskmaster in a parent directory.
|
||||
|
||||
const projectDir = tempDir;
|
||||
const srcDir = path.join(projectDir, 'src');
|
||||
|
||||
fs.mkdirSync(path.join(projectDir, '.taskmaster'));
|
||||
fs.mkdirSync(srcDir);
|
||||
|
||||
const result = findProjectRoot(srcDir);
|
||||
|
||||
// Immediate parent (depth=1) is trusted even without boundary marker
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
|
||||
it('should return startDir when running from completely empty directory', () => {
|
||||
// Scenario: User runs `tm init` in a brand new, completely empty directory
|
||||
// No markers anywhere in the tree
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/test/empty-project/ (empty)
|
||||
|
||||
const emptyProjectDir = path.join(tempDir, 'empty-project');
|
||||
fs.mkdirSync(emptyProjectDir);
|
||||
|
||||
const result = findProjectRoot(emptyProjectDir);
|
||||
|
||||
// Should return the empty directory itself
|
||||
expect(result).toBe(emptyProjectDir);
|
||||
});
|
||||
|
||||
it('should return startDir when no boundary markers exist but .taskmaster exists in distant parent', () => {
|
||||
// Scenario: Deep directory structure with .taskmaster only at top level
|
||||
// No boundary markers (.git, package.json, etc.) anywhere
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/home/.taskmaster (should be IGNORED - too far up)
|
||||
// /tmp/home/code/projects/my-app/ (empty - no markers)
|
||||
|
||||
const homeDir = tempDir;
|
||||
const deepProjectDir = path.join(homeDir, 'code', 'projects', 'my-app');
|
||||
|
||||
fs.mkdirSync(path.join(homeDir, '.taskmaster'));
|
||||
fs.mkdirSync(deepProjectDir, { recursive: true });
|
||||
|
||||
const result = findProjectRoot(deepProjectDir);
|
||||
|
||||
// Should return the deep directory, not home
|
||||
expect(result).toBe(deepProjectDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Project with boundary markers', () => {
|
||||
it('should find .taskmaster when at same level as .git', () => {
|
||||
// Scenario: Normal project setup with both .taskmaster and .git
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/project/.taskmaster
|
||||
// /tmp/project/.git
|
||||
|
||||
const projectDir = tempDir;
|
||||
|
||||
fs.mkdirSync(path.join(projectDir, '.taskmaster'));
|
||||
fs.mkdirSync(path.join(projectDir, '.git'));
|
||||
|
||||
const result = findProjectRoot(projectDir);
|
||||
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
|
||||
it('should find .taskmaster in parent when subdirectory has no markers', () => {
|
||||
// Scenario: Running from a subdirectory of an initialized project
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/project/.taskmaster
|
||||
// /tmp/project/.git
|
||||
// /tmp/project/src/components/ (no markers)
|
||||
|
||||
const projectDir = tempDir;
|
||||
const srcDir = path.join(projectDir, 'src', 'components');
|
||||
|
||||
fs.mkdirSync(path.join(projectDir, '.taskmaster'));
|
||||
fs.mkdirSync(path.join(projectDir, '.git'));
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
|
||||
const result = findProjectRoot(srcDir);
|
||||
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
|
||||
it('should stop at .git and NOT find .taskmaster beyond it', () => {
|
||||
// Scenario: Project has .git but no .taskmaster, parent has .taskmaster
|
||||
// Should use project with .git, not parent with .taskmaster
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/home/.taskmaster (should be IGNORED)
|
||||
// /tmp/home/my-project/.git (boundary marker)
|
||||
|
||||
const homeDir = tempDir;
|
||||
const projectDir = path.join(homeDir, 'my-project');
|
||||
|
||||
fs.mkdirSync(path.join(homeDir, '.taskmaster'));
|
||||
fs.mkdirSync(path.join(projectDir, '.git'), { recursive: true });
|
||||
|
||||
const result = findProjectRoot(projectDir);
|
||||
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
|
||||
it('should stop at package.json and NOT find .taskmaster beyond it', () => {
|
||||
// Scenario: JS project with package.json but no .taskmaster
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/home/.taskmaster (should be IGNORED)
|
||||
// /tmp/home/my-project/package.json (boundary)
|
||||
|
||||
const homeDir = tempDir;
|
||||
const projectDir = path.join(homeDir, 'my-project');
|
||||
|
||||
fs.mkdirSync(path.join(homeDir, '.taskmaster'));
|
||||
fs.mkdirSync(projectDir);
|
||||
fs.writeFileSync(path.join(projectDir, 'package.json'), '{}');
|
||||
|
||||
const result = findProjectRoot(projectDir);
|
||||
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Monorepo scenarios', () => {
|
||||
it('should find monorepo root .taskmaster from package subdirectory', () => {
|
||||
// Scenario: Monorepo with .taskmaster at root, packages without their own markers
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/monorepo/.taskmaster
|
||||
// /tmp/monorepo/.git
|
||||
// /tmp/monorepo/packages/my-package/src/
|
||||
|
||||
const monorepoRoot = tempDir;
|
||||
const packageSrcDir = path.join(
|
||||
monorepoRoot,
|
||||
'packages',
|
||||
'my-package',
|
||||
'src'
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.join(monorepoRoot, '.taskmaster'));
|
||||
fs.mkdirSync(path.join(monorepoRoot, '.git'));
|
||||
fs.mkdirSync(packageSrcDir, { recursive: true });
|
||||
|
||||
const result = findProjectRoot(packageSrcDir);
|
||||
|
||||
expect(result).toBe(monorepoRoot);
|
||||
});
|
||||
|
||||
it('should return package root when package has its own boundary marker', () => {
|
||||
// Scenario: Monorepo where individual package has its own package.json
|
||||
// Package should be treated as its own project
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/monorepo/.taskmaster
|
||||
// /tmp/monorepo/packages/my-package/package.json (boundary)
|
||||
|
||||
const monorepoRoot = tempDir;
|
||||
const packageDir = path.join(monorepoRoot, 'packages', 'my-package');
|
||||
|
||||
fs.mkdirSync(path.join(monorepoRoot, '.taskmaster'));
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(packageDir, 'package.json'), '{}');
|
||||
|
||||
const result = findProjectRoot(packageDir);
|
||||
|
||||
// Package has its own boundary, so it should be returned
|
||||
expect(result).toBe(packageDir);
|
||||
});
|
||||
|
||||
it('should return package root when package has its own .taskmaster', () => {
|
||||
// Scenario: Nested Task Master initialization
|
||||
//
|
||||
// Structure:
|
||||
// /tmp/monorepo/.taskmaster
|
||||
// /tmp/monorepo/packages/my-package/.taskmaster
|
||||
|
||||
const monorepoRoot = tempDir;
|
||||
const packageDir = path.join(monorepoRoot, 'packages', 'my-package');
|
||||
|
||||
fs.mkdirSync(path.join(monorepoRoot, '.taskmaster'));
|
||||
fs.mkdirSync(path.join(packageDir, '.taskmaster'), { recursive: true });
|
||||
|
||||
const result = findProjectRoot(packageDir);
|
||||
|
||||
// Package has its own .taskmaster, so it should be returned
|
||||
expect(result).toBe(packageDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment variable loading context', () => {
|
||||
it('should document that .env loading should use process.cwd(), not findProjectRoot()', () => {
|
||||
// IMPORTANT: For .env loading (e.g., TM_BASE_DOMAIN for auth),
|
||||
// the code should use process.cwd() directly, NOT findProjectRoot().
|
||||
//
|
||||
// findProjectRoot() is designed to find the .taskmaster directory
|
||||
// for task storage. It will traverse up to find .taskmaster in parent
|
||||
// directories when appropriate.
|
||||
//
|
||||
// For environment variables, we want to load from WHERE the user
|
||||
// is running the command, not where .taskmaster is located.
|
||||
//
|
||||
// This test documents this design decision and verifies findProjectRoot
|
||||
// behavior when .taskmaster is in immediate parent (depth=1).
|
||||
|
||||
const projectDir = tempDir;
|
||||
const subDir = path.join(projectDir, 'subdir');
|
||||
|
||||
fs.mkdirSync(path.join(projectDir, '.taskmaster'));
|
||||
fs.mkdirSync(subDir);
|
||||
|
||||
const result = findProjectRoot(subDir);
|
||||
|
||||
// findProjectRoot WILL find parent's .taskmaster (depth=1 is trusted)
|
||||
// This is correct for task storage - we want to use the parent's tasks.json
|
||||
// For .env loading, callers should use process.cwd() instead
|
||||
expect(result).toBe(projectDir);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* @fileoverview Project root detection utilities
|
||||
* Provides functionality to locate project roots by searching for marker files/directories
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
@@ -11,9 +6,6 @@ import {
|
||||
TASKMASTER_PROJECT_MARKERS
|
||||
} from '../constants/paths.js';
|
||||
|
||||
/**
|
||||
* Check if a marker file/directory exists at the given path
|
||||
*/
|
||||
function markerExists(dir: string, marker: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(path.join(dir, marker));
|
||||
@@ -22,170 +14,101 @@ function markerExists(dir: string, marker: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the given markers exist in a directory
|
||||
*/
|
||||
function hasAnyMarker(dir: string, markers: readonly string[]): boolean {
|
||||
return markers.some((marker) => markerExists(dir, marker));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the project root directory by looking for project markers
|
||||
* 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
|
||||
* Find the project root by traversing upward from startDir looking for project markers.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. PASS 1: Search for .taskmaster markers, but STOP at project boundaries
|
||||
* - If .taskmaster found, return that directory
|
||||
* - If a project boundary (package.json, .git, lock files) is found WITHOUT .taskmaster,
|
||||
* stop searching further up (prevents finding .taskmaster in home directory)
|
||||
* 2. PASS 2: If no .taskmaster found, search for other project markers
|
||||
*
|
||||
* This ensures:
|
||||
* - .taskmaster in a parent directory takes precedence (within project boundary)
|
||||
* - .taskmaster outside the project boundary (e.g., home dir) is NOT returned
|
||||
*
|
||||
* @param startDir - Directory to start searching from (defaults to process.cwd())
|
||||
* @returns Project root path (falls back to startDir if no markers found)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In a monorepo structure:
|
||||
* // /project/.taskmaster
|
||||
* // /project/packages/my-package/.git
|
||||
* // When called from /project/packages/my-package:
|
||||
* const root = findProjectRoot(); // Returns /project (not /project/packages/my-package)
|
||||
*
|
||||
* // When .taskmaster is outside project boundary:
|
||||
* // /home/user/.taskmaster (should be ignored!)
|
||||
* // /home/user/code/myproject/package.json
|
||||
* // When called from /home/user/code/myproject:
|
||||
* const root = findProjectRoot(); // Returns /home/user/code/myproject (NOT /home/user)
|
||||
* ```
|
||||
* Search strategy prevents false matches from stray .taskmaster dirs (e.g., in home):
|
||||
* 1. If startDir has .taskmaster or a boundary marker (.git, package.json), return immediately
|
||||
* 2. Search parents for .taskmaster anchored by a boundary marker (or 1 level up without boundary)
|
||||
* 3. Fall back to searching for other project markers (pyproject.toml, Cargo.toml, etc.)
|
||||
* 4. If nothing found, return startDir (supports `tm init` in empty directories)
|
||||
*/
|
||||
export function findProjectRoot(startDir: string = process.cwd()): string {
|
||||
let currentDir = path.resolve(startDir);
|
||||
const rootDir = path.parse(currentDir).root;
|
||||
const maxDepth = 50; // Reasonable limit to prevent infinite loops
|
||||
const maxDepth = 50;
|
||||
let depth = 0;
|
||||
|
||||
// Track if we've seen a project boundary - we'll stop searching for .taskmaster beyond it
|
||||
let projectBoundaryDir: string | null = null;
|
||||
|
||||
// FIRST PASS: Search for Task Master markers, but respect project boundaries
|
||||
// A project boundary is a directory containing .git, package.json, lock files, etc.
|
||||
// If we find a boundary without .taskmaster, we stop searching further up
|
||||
let searchDir = currentDir;
|
||||
depth = 0;
|
||||
// Check startDir first - if it has .taskmaster or a boundary marker, we're done
|
||||
if (hasAnyMarker(currentDir, TASKMASTER_PROJECT_MARKERS)) {
|
||||
return currentDir;
|
||||
}
|
||||
if (hasAnyMarker(currentDir, PROJECT_BOUNDARY_MARKERS)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
// Search parent directories for .taskmaster
|
||||
let searchDir = path.dirname(currentDir);
|
||||
depth = 1;
|
||||
|
||||
while (depth < maxDepth) {
|
||||
// First, check for Task Master markers in this directory
|
||||
for (const marker of TASKMASTER_PROJECT_MARKERS) {
|
||||
if (markerExists(searchDir, marker)) {
|
||||
// Found a Task Master marker - this is our project root
|
||||
const hasTaskmaster = hasAnyMarker(searchDir, TASKMASTER_PROJECT_MARKERS);
|
||||
const hasBoundary = hasAnyMarker(searchDir, PROJECT_BOUNDARY_MARKERS);
|
||||
|
||||
if (hasTaskmaster) {
|
||||
// Accept .taskmaster if anchored by boundary or only 1 level up
|
||||
if (hasBoundary || depth === 1) {
|
||||
return searchDir;
|
||||
}
|
||||
// Distant .taskmaster without boundary is likely stray (e.g., home dir) - skip it
|
||||
}
|
||||
|
||||
// Check if this directory is a project boundary
|
||||
// (has markers like .git, package.json, lock files, etc.)
|
||||
if (hasAnyMarker(searchDir, PROJECT_BOUNDARY_MARKERS)) {
|
||||
// This is a project boundary - record it and STOP looking for .taskmaster
|
||||
// beyond this point. The .taskmaster in home directory should NOT be found
|
||||
// when the user is inside a different project.
|
||||
if (hasBoundary && !hasTaskmaster) {
|
||||
// Hit project boundary without .taskmaster - stop searching upward
|
||||
projectBoundaryDir = searchDir;
|
||||
break; // Stop Pass 1 - don't look for .taskmaster beyond this boundary
|
||||
}
|
||||
|
||||
// If we're at root, stop after checking it
|
||||
if (searchDir === rootDir) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Move up one directory level
|
||||
if (searchDir === rootDir) break;
|
||||
|
||||
const parentDir = path.dirname(searchDir);
|
||||
|
||||
// Safety check: if dirname returns the same path, we've hit the root
|
||||
if (parentDir === searchDir) {
|
||||
break;
|
||||
}
|
||||
if (parentDir === searchDir) break;
|
||||
|
||||
searchDir = parentDir;
|
||||
depth++;
|
||||
}
|
||||
|
||||
// SECOND PASS: No Task Master markers found within project boundary
|
||||
// Now search for other project markers starting from the original directory
|
||||
// If we found a project boundary in Pass 1, start from there (it will match immediately)
|
||||
// No .taskmaster found - search for other project markers
|
||||
currentDir = projectBoundaryDir || path.resolve(startDir);
|
||||
depth = 0;
|
||||
|
||||
while (depth < maxDepth) {
|
||||
for (const marker of OTHER_PROJECT_MARKERS) {
|
||||
if (markerExists(currentDir, marker)) {
|
||||
// Found another project marker - return this as project root
|
||||
return currentDir;
|
||||
}
|
||||
if (hasAnyMarker(currentDir, OTHER_PROJECT_MARKERS)) {
|
||||
return currentDir;
|
||||
}
|
||||
|
||||
// If we're at root, stop after checking it
|
||||
if (currentDir === rootDir) {
|
||||
break;
|
||||
}
|
||||
if (currentDir === rootDir) break;
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (parentDir === currentDir) break;
|
||||
|
||||
currentDir = parentDir;
|
||||
depth++;
|
||||
}
|
||||
|
||||
// Fallback to startDir if no project root found
|
||||
// This handles empty repos or directories with no recognized project markers
|
||||
// (e.g., a repo with just a .env file should still use that directory as root)
|
||||
return path.resolve(startDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize project root to ensure it doesn't end with .taskmaster
|
||||
* This prevents double .taskmaster paths when using constants that include .taskmaster
|
||||
*
|
||||
* @param projectRoot - The project root path to normalize
|
||||
* @returns Normalized project root path
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* normalizeProjectRoot('/project/.taskmaster'); // Returns '/project'
|
||||
* normalizeProjectRoot('/project'); // Returns '/project'
|
||||
* normalizeProjectRoot('/project/.taskmaster/tasks'); // Returns '/project'
|
||||
* ```
|
||||
* Strip .taskmaster (and anything after it) from a path.
|
||||
* Prevents double .taskmaster paths when combining with constants that include .taskmaster.
|
||||
*/
|
||||
export function normalizeProjectRoot(
|
||||
projectRoot: string | null | undefined
|
||||
): string {
|
||||
if (!projectRoot) return projectRoot || '';
|
||||
if (!projectRoot) return '';
|
||||
|
||||
// Ensure it's a string
|
||||
const projectRootStr = String(projectRoot);
|
||||
|
||||
// Split the path into segments
|
||||
const segments = projectRootStr.split(path.sep);
|
||||
|
||||
// Find the index of .taskmaster segment
|
||||
const taskmasterIndex = segments.findIndex(
|
||||
(segment) => segment === '.taskmaster'
|
||||
);
|
||||
const segments = String(projectRoot).split(path.sep);
|
||||
const taskmasterIndex = segments.findIndex((s) => s === '.taskmaster');
|
||||
|
||||
if (taskmasterIndex !== -1) {
|
||||
// If .taskmaster is found, return everything up to but not including .taskmaster
|
||||
const normalizedSegments = segments.slice(0, taskmasterIndex);
|
||||
return normalizedSegments.join(path.sep) || path.sep;
|
||||
return segments.slice(0, taskmasterIndex).join(path.sep) || path.sep;
|
||||
}
|
||||
|
||||
return projectRootStr;
|
||||
return String(projectRoot);
|
||||
}
|
||||
|
||||
@@ -93,9 +93,12 @@ describe('initTaskMaster', () => {
|
||||
});
|
||||
|
||||
test('should find project root from deeply nested subdirectory', () => {
|
||||
// Arrange - Create .taskmaster directory in temp dir
|
||||
// Arrange - Create .taskmaster directory and a boundary marker in temp dir
|
||||
// The boundary marker (.git) anchors .taskmaster to prevent false matches
|
||||
// from stray .taskmaster dirs (e.g., in home directory)
|
||||
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
|
||||
fs.mkdirSync(taskMasterDir, { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.git'), { recursive: true });
|
||||
|
||||
// Create deeply nested subdirectory and change to it
|
||||
const deepDir = path.join(tempDir, 'src', 'components', 'ui');
|
||||
|
||||
Reference in New Issue
Block a user