Spaces:
Sleeping
Sleeping
| 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; | |
| }; | |