tree-ast-grep-mcp
Version:
Simple, direct ast-grep wrapper for AI coding agents. Zero abstractions, maximum performance.
298 lines • 12 kB
JavaScript
import * as fs from 'fs/promises';
import * as fsSync from 'fs';
import * as path from 'path';
/**
* Detects and manages workspace boundaries used by MCP tools.
*/
export class WorkspaceManager {
config;
/**
* Build workspace configuration from an optional explicit root.
*/
constructor(explicitRoot) {
this.config = this.detectWorkspace(explicitRoot);
}
/**
* Determine the effective workspace root and allowed path boundaries.
*/
detectWorkspace(explicitRoot) {
let workspaceRoot;
if (explicitRoot) {
workspaceRoot = path.resolve(explicitRoot);
}
else {
// Auto-detect workspace root
workspaceRoot = this.autoDetectWorkspaceRoot();
}
return {
root: workspaceRoot,
allowedPaths: [],
blockedPaths: this.getBlockedPaths(),
maxDepth: 10,
};
}
autoDetectWorkspaceRoot() {
// Priority 1: Use explicitly set WORKSPACE_ROOT environment variable
if (process.env.WORKSPACE_ROOT) {
const explicitRoot = path.resolve(process.env.WORKSPACE_ROOT);
console.error(`Using explicit workspace root: ${explicitRoot}`);
return explicitRoot;
}
let currentDir = process.cwd();
console.error(`Starting workspace detection from: ${currentDir}`);
// Enhanced root indicators with priority ordering
const primaryIndicators = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pom.xml'];
const secondaryIndicators = ['pyproject.toml', 'composer.json', 'build.gradle', 'tsconfig.json'];
const tertiaryIndicators = ['Makefile', 'README.md', '.vscode', '.idea', 'Gemfile'];
const allIndicators = [...primaryIndicators, ...secondaryIndicators, ...tertiaryIndicators];
// Enhanced detection with validation - increased search depth to 8 levels
for (let depth = 0; depth <= 8; depth++) {
// Try primary indicators first (most reliable)
for (const indicator of primaryIndicators) {
try {
fsSync.accessSync(path.join(currentDir, indicator));
if (this.validateWorkspaceRoot(currentDir)) {
console.error(`Found primary workspace indicator '${indicator}' in: ${currentDir}`);
return currentDir;
}
}
catch {
// Indicator not found, continue
}
}
// Try secondary indicators if no primary found
for (const indicator of secondaryIndicators) {
try {
fsSync.accessSync(path.join(currentDir, indicator));
if (this.validateWorkspaceRoot(currentDir)) {
console.error(`Found secondary workspace indicator '${indicator}' in: ${currentDir}`);
return currentDir;
}
}
catch {
// Indicator not found, continue
}
}
// Try tertiary indicators as last resort
if (depth >= 2) { // Only check tertiary after going up a bit
for (const indicator of tertiaryIndicators) {
try {
fsSync.accessSync(path.join(currentDir, indicator));
if (this.validateWorkspaceRoot(currentDir)) {
console.error(`Found tertiary workspace indicator '${indicator}' in: ${currentDir}`);
return currentDir;
}
}
catch {
// Indicator not found, continue
}
}
}
// Move up one directory
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir)
break; // Reached filesystem root
currentDir = parentDir;
}
// Enhanced fallback: use current directory with validation
const fallback = process.cwd();
console.error(`No workspace indicators found, using current directory: ${fallback}`);
return fallback;
}
validateWorkspaceRoot(rootPath) {
try {
const entries = fsSync.readdirSync(rootPath);
// Check for presence of source code files or directories
const codeIndicators = ['src', 'lib', 'app', 'components', 'modules', 'source', 'Sources'];
const hasCodeStructure = entries.some(entry => {
try {
const entryPath = path.join(rootPath, entry);
const stat = fsSync.statSync(entryPath);
if (stat.isDirectory() && codeIndicators.includes(entry)) {
return true;
}
if (stat.isFile() && entry.match(/\.(js|ts|jsx|tsx|py|java|rs|go|cpp|c|h|php|rb)$/i)) {
return true;
}
}
catch {
// Skip entries we can't stat
}
return false;
});
return hasCodeStructure;
}
catch {
return false; // If we can't read the directory, assume it's not a valid workspace
}
}
getBlockedPaths() {
const systemPaths = [
'/etc', '/bin', '/usr', '/sys', '/proc', // Unix system dirs
'C:\\Windows', 'C:\\Program Files', // Windows system dirs
path.join(process.env.HOME || '', '.ssh'), // SSH keys
path.join(process.env.HOME || '', '.aws'), // AWS credentials
'node_modules/.bin', // Binary executables
'.git', // Git internal files
];
return systemPaths.map(p => path.resolve(p));
}
getConfig() {
return { ...this.config };
}
/**
* Expose the root directory used for workspace relative operations.
*/
getWorkspaceRoot() {
return this.config.root;
}
validatePath(inputPath) {
try {
// Resolve the path relative to workspace root
const resolvedPath = path.resolve(this.config.root, inputPath);
const normalizedRoot = path.resolve(this.config.root);
const relativeFromRoot = path.relative(normalizedRoot, resolvedPath);
// Ensure the resolved path is within the workspace root
if (relativeFromRoot === '' ||
relativeFromRoot === '.') {
// resolvedPath is the root itself; allow
}
else if (relativeFromRoot.startsWith('..' + path.sep) ||
relativeFromRoot === '..') {
return {
valid: false,
resolvedPath,
error: `Path "${inputPath}" is outside workspace root`
};
}
// Check against blocked paths
for (const blockedPath of this.config.blockedPaths) {
if (resolvedPath.startsWith(blockedPath)) {
return {
valid: false,
resolvedPath,
error: `Access to system directory "${inputPath}" is blocked`
};
}
}
// Check depth limit
const relativePath = relativeFromRoot;
const depth = relativePath.split(path.sep).length;
if (depth > this.config.maxDepth) {
return {
valid: false,
resolvedPath,
error: `Path depth (${depth}) exceeds maximum allowed depth (${this.config.maxDepth})`
};
}
return {
valid: true,
resolvedPath
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
valid: false,
resolvedPath: inputPath,
error: `Invalid path: ${errorMessage}`
};
}
}
validatePaths(inputPaths) {
const resolvedPaths = [];
const errors = [];
let allValid = true;
for (const inputPath of inputPaths) {
const validation = this.validatePath(inputPath);
if (validation.valid) {
resolvedPaths.push(validation.resolvedPath);
}
else {
allValid = false;
errors.push(validation.error || `Invalid path: ${inputPath}`);
}
}
return {
valid: allValid,
resolvedPaths,
errors
};
}
// Get all files in the workspace (with safety limits)
async getWorkspaceFiles(options = {}) {
const { includePatterns = [], excludePatterns = ['node_modules', '.git', 'build', 'dist'], maxFiles = 100000 } = options;
const files = [];
const visited = new Set();
const scanDirectory = async (dirPath, currentDepth = 0) => {
if (currentDepth > this.config.maxDepth)
return;
if (files.length >= maxFiles)
return;
try {
const items = await fs.readdir(dirPath);
for (const item of items) {
if (files.length >= maxFiles)
break;
const itemPath = path.join(dirPath, item);
const relativePath = path.relative(this.config.root, itemPath);
// Skip if already visited (symlink protection)
if (visited.has(itemPath))
continue;
visited.add(itemPath);
// Check exclude patterns
if (excludePatterns.some(pattern => {
return relativePath.includes(pattern) ||
item.startsWith('.') && pattern === '.*';
})) {
continue;
}
const stats = await fs.stat(itemPath);
if (stats.isFile()) {
// Check include patterns if specified
if (includePatterns.length === 0 ||
includePatterns.some(pattern => relativePath.includes(pattern))) {
files.push(itemPath);
}
}
else if (stats.isDirectory()) {
await scanDirectory(itemPath, currentDepth + 1);
}
}
}
catch (error) {
// Skip directories we can't read
}
};
await scanDirectory(this.config.root);
return files;
}
async countFilesRecursive(dir, currentDepth = 0) {
if (currentDepth > this.config.maxDepth)
return 0;
let count = 0;
try {
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
try {
const stats = await fs.stat(itemPath);
if (stats.isFile()) {
count++;
}
else if (stats.isDirectory() && !item.startsWith('.')) {
count += await this.countFilesRecursive(itemPath, currentDepth + 1);
}
}
catch {
// Skip inaccessible files
}
}
}
catch {
// Skip inaccessible directories
}
return count;
}
}
//# sourceMappingURL=workspace-manager.js.map