mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
246 lines
8.1 KiB
TypeScript
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;
|
|
};
|