Files
mcphub/src/utils/path.ts

246 lines
8.1 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { dirname } from 'path';
import { getCurrentModuleDir } from './moduleDir.js';
// Project root directory - use process.cwd() as a simpler alternative
const rootDir = process.cwd();
// Cache the package root for performance
let cachedPackageRoot: string | null | undefined = undefined;
/**
* Initialize package root by trying to find it using the module directory
* This should be called when the module is first loaded
*/
function initializePackageRoot(): void {
// Skip initialization in test environments
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined) {
return;
}
try {
// Try to get the current module's directory
const currentModuleDir = getCurrentModuleDir();
// This file is in src/utils/path.ts (or dist/utils/path.js when compiled)
// So package.json should be 2 levels up
const possibleRoots = [
path.resolve(currentModuleDir, '..', '..'), // dist -> package root
path.resolve(currentModuleDir, '..'), // dist/utils -> dist -> package root
];
for (const root of possibleRoots) {
const packageJsonPath = path.join(root, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
cachedPackageRoot = root;
return;
}
} catch {
// Continue checking
}
}
}
} catch {
// If initialization fails, cachedPackageRoot remains undefined
// and findPackageRoot will search normally
}
}
// Initialize on module load (unless in test environment)
initializePackageRoot();
/**
* Find the package root directory (where package.json is located)
* This works correctly when the package is installed globally or locally
* @param startPath Starting path to search from (defaults to checking module paths)
* @returns The package root directory path, or null if not found
*/
export const findPackageRoot = (startPath?: string): string | null => {
// Return cached value if available and no specific start path is requested
if (cachedPackageRoot !== undefined && !startPath) {
return cachedPackageRoot;
}
const debug = process.env.DEBUG === 'true';
// Possible locations for package.json relative to the search path
const possibleRoots: string[] = [];
if (startPath) {
// When start path is provided (from fileURLToPath(import.meta.url))
possibleRoots.push(
// When in dist/utils (compiled code) - go up 2 levels
path.resolve(startPath, '..', '..'),
// When in dist/ (compiled code) - go up 1 level
path.resolve(startPath, '..'),
// Direct parent directories
path.resolve(startPath),
);
}
// Try to use require.resolve to find the module location (works in CommonJS and ESM with createRequire)
try {
// In ESM, we can use import.meta.resolve, but it's async in some versions
// So we'll try to find the module by checking the node_modules structure
// Check if this file is in a node_modules installation
const currentFile = new Error().stack?.split('\n')[2]?.match(/\((.+?):\d+:\d+\)$/)?.[1];
if (currentFile) {
const nodeModulesIndex = currentFile.indexOf('node_modules');
if (nodeModulesIndex !== -1) {
// Extract the package path from node_modules
const afterNodeModules = currentFile.substring(
nodeModulesIndex + 'node_modules'.length + 1,
);
const packageNameEnd = afterNodeModules.indexOf(path.sep);
if (packageNameEnd !== -1) {
const packagePath = currentFile.substring(
0,
nodeModulesIndex + 'node_modules'.length + 1 + packageNameEnd,
);
possibleRoots.push(packagePath);
}
}
}
} catch {
// Ignore errors
}
// Check module.filename location (works in Node.js when available)
if (typeof __filename !== 'undefined') {
const moduleDir = path.dirname(__filename);
possibleRoots.push(path.resolve(moduleDir, '..', '..'), path.resolve(moduleDir, '..'));
}
// Check common installation locations
possibleRoots.push(
// Current working directory (for development/tests)
process.cwd(),
// Parent of cwd
path.resolve(process.cwd(), '..'),
);
if (debug) {
console.log('DEBUG: Searching for package.json from:', startPath || 'multiple locations');
console.log('DEBUG: Checking paths:', possibleRoots);
}
// Remove duplicates
const uniqueRoots = [...new Set(possibleRoots)];
for (const root of uniqueRoots) {
const packageJsonPath = path.join(root, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (pkg.name === 'mcphub' || pkg.name === '@samanhappy/mcphub') {
if (debug) {
console.log(`DEBUG: Found package.json at ${packageJsonPath}`);
}
// Cache the result if no specific start path was requested
if (!startPath) {
cachedPackageRoot = root;
}
return root;
}
} catch (e) {
// Continue to the next potential root
if (debug) {
console.error(`DEBUG: Failed to parse package.json at ${packageJsonPath}:`, e);
}
}
}
}
if (debug) {
console.warn('DEBUG: Could not find package root directory');
}
// Cache null result as well to avoid repeated searches
if (!startPath) {
cachedPackageRoot = null;
}
return null;
};
function getParentPath(p: string, filename: string): string {
if (p.endsWith(filename)) {
p = p.slice(0, -filename.length);
}
return path.resolve(p);
}
/**
* Find the path to a configuration file by checking multiple potential locations.
* @param filename The name of the file to locate (e.g., 'servers.json', 'mcp_settings.json')
* @param description Brief description of the file for logging purposes
* @returns The path to the file
*/
export const getConfigFilePath = (filename: string, description = 'Configuration'): string => {
if (filename === 'mcp_settings.json') {
const envPath = process.env.MCPHUB_SETTING_PATH;
if (envPath) {
// Ensure directory exists
const dir = getParentPath(envPath, filename);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`Created directory for settings at ${dir}`);
}
// if full path, return as is
if (envPath?.endsWith(filename)) {
return envPath;
}
// if directory, return path under that directory
return path.resolve(envPath, filename);
}
}
const potentialPaths = [
// Prioritize process.cwd() as the first location to check
path.resolve(process.cwd(), filename),
// Use path relative to the root directory
path.join(rootDir, filename),
// If installed with npx, may need to look one level up
path.join(dirname(rootDir), filename),
];
// Also check in the installed package root directory
const packageRoot = findPackageRoot();
if (packageRoot) {
potentialPaths.push(path.join(packageRoot, filename));
}
for (const filePath of potentialPaths) {
if (fs.existsSync(filePath)) {
return filePath;
}
}
// If all paths do not exist, check if we have a fallback in the package root
// If the file exists in the package root, use it as the default
if (packageRoot) {
const packageConfigPath = path.join(packageRoot, filename);
if (fs.existsSync(packageConfigPath)) {
console.log(`Using ${description} from package: ${packageConfigPath}`);
return packageConfigPath;
}
}
// If all paths do not exist, use default path
// Using the default path is acceptable because it ensures the application can proceed
// even if the configuration file is missing. This fallback is particularly useful in
// development environments or when the file is optional.
const defaultPath = path.resolve(process.cwd(), filename);
console.debug(
`${description} file not found at any expected location, using default path: ${defaultPath}`,
);
return defaultPath;
};