File size: 8,312 Bytes
07af8f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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;
};