sql-talk
Version:
SQL Talk - 自然言語をSQLに変換するMCPサーバー(安全性保護・SSHトンネル対応) / SQL Talk - MCP Server for Natural Language to SQL conversion with safety guards and SSH tunnel support
213 lines • 9.45 kB
JavaScript
import { readdirSync, statSync } from 'fs';
import { dirname, join } from 'path';
import chalk from 'chalk';
import { logger } from './logger.js';
/**
* ワークスペース検知ユーティリティ
* Workspace detection utility
*/
export class WorkspaceDetector {
/**
* プロジェクトルートを検出
* Detect project root directory
*/
static detectProjectRoot() {
logger.info('🔍 Detecting project root...');
// 利用可能な環境変数をログに出力
console.error(chalk.blue('🔍 Available environment variables:'));
const relevantEnvVars = [
'CURSOR_WORKSPACE_ROOT',
'VSCODE_WORKSPACE_FOLDER',
'WORKSPACE_FOLDER_PATHS', // Cursorが設定する環境変数
'PROJECT_ROOT',
'PWD',
'CWD',
'WORKSPACE_ROOT',
'PROJECT_DIR',
'CURSOR_PROJECT_ROOT',
'CURSOR_CURRENT_PROJECT',
'CURSOR_OPENED_PROJECT',
'VSCODE_CWD',
'VSCODE_PID',
'VSCODE_INJECTION'
];
relevantEnvVars.forEach(envVar => {
const value = process.env[envVar];
if (value) {
console.error(chalk.green(` ${envVar}:`), value);
}
else {
console.error(chalk.gray(` ${envVar}:`), chalk.red('(not set)'));
}
});
// 全ての環境変数の中で、パス関連のものを表示
console.error(chalk.blue('🔍 All path-related environment variables:'));
Object.keys(process.env).forEach(key => {
if (key.includes('PATH') || key.includes('ROOT') || key.includes('DIR') || key.includes('WORKSPACE') || key.includes('PROJECT')) {
const value = process.env[key];
if (value && value.includes('/')) {
console.error(chalk.cyan(` ${key}:`), value);
}
}
});
console.error(chalk.blue('🔍 process.cwd():'), process.cwd());
console.error(chalk.blue('🔍 __dirname:'), __dirname);
// プロジェクトルート検出の優先順位
const candidates = [
process.env.CURSOR_WORKSPACE_ROOT,
process.env.VSCODE_WORKSPACE_FOLDER,
process.env.WORKSPACE_FOLDER_PATHS, // Cursorが設定する環境変数
process.env.PROJECT_ROOT,
process.env.PWD,
process.cwd()
].filter((value) => Boolean(value));
console.error(chalk.blue('🔍 Project root candidates:'));
candidates.forEach((candidate, index) => {
console.error(chalk.cyan(` ${index + 1}.`), candidate);
});
// 候補を検証して最初の有効なものを返す
for (const candidate of candidates) {
if (this.isValidProjectRoot(candidate)) {
console.error(chalk.green(`✅ Valid project root found: ${candidate}`));
console.error(chalk.yellow('⚠️ Initial PROJECT_ROOT (will be overridden by config):'), candidate);
return candidate;
}
}
// 有効な候補が見つからない場合は、現在の作業ディレクトリからプロジェクトルートを自動検出
console.error(chalk.yellow('⚠️ No valid project root found in candidates, auto-detecting from current directory...'));
let currentDir = process.cwd();
let attempts = 0;
const maxAttempts = 10; // 最大10階層まで遡る
console.error(chalk.blue('🔍 Starting project root detection from:'), currentDir);
console.error(chalk.blue('🔍 This is the directory where MCP server was started'));
while (attempts < maxAttempts) {
console.error(chalk.gray(` Attempt ${attempts + 1}: ${currentDir}`));
// このディレクトリの内容を確認
try {
const dirContents = readdirSync(currentDir);
console.error(chalk.gray(` Directory contents: ${dirContents.slice(0, 10).join(', ')}${dirContents.length > 10 ? '...' : ''}`));
}
catch (error) {
console.error(chalk.red(` Error reading directory: ${error instanceof Error ? error.message : String(error)}`));
}
if (this.isValidProjectRoot(currentDir)) {
console.error(chalk.green(`✅ Valid project root found: ${currentDir}`));
console.error(chalk.blue('🎯 This directory contains package.json or .git, indicating it is a project root'));
console.error(chalk.yellow('⚠️ Initial PROJECT_ROOT (will be overridden by config):'), currentDir);
return currentDir;
}
const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
console.error(chalk.red('❌ Reached root directory, stopping search'));
break; // ルートディレクトリに到達
}
currentDir = parentDir;
attempts++;
}
// 最後の手段としてprocess.cwd()を返す
const fallbackRoot = process.cwd();
console.error(chalk.red(`❌ No valid project root found, using fallback: ${fallbackRoot}`));
console.error(chalk.yellow('⚠️ Initial PROJECT_ROOT (will be overridden by config):'), fallbackRoot);
return fallbackRoot;
}
/**
* プロジェクトルートが有効かチェック
* Check if directory is a valid project root
*/
static isValidProjectRoot(dirPath) {
try {
const stats = statSync(dirPath);
if (!stats.isDirectory()) {
return false;
}
const contents = readdirSync(dirPath);
// package.json または .git ディレクトリがあるかチェック
const hasPackageJson = contents.includes('package.json');
const hasGitDir = contents.includes('.git');
const hasConfigFile = contents.includes('sql-talk.config.yaml') || contents.includes('sql-talk.config.yml');
logger.debug(` Checking ${dirPath}: package.json=${hasPackageJson}, .git=${hasGitDir}, config=${hasConfigFile}`);
// package.json または .git があれば有効なプロジェクトルートとみなす
return hasPackageJson || hasGitDir;
}
catch (error) {
logger.debug(` Error checking ${dirPath}: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* ワークスペース内の設定ファイルパスを取得
* Get configuration file path within workspace
*/
static getConfigPath(workspaceRoot) {
const root = workspaceRoot || this.detectProjectRoot();
// 設定ファイルの候補パス
const configCandidates = [
join(root, 'sql-talk.config.yaml'),
join(root, 'sql-talk.config.yml'),
join(root, 'config', 'sql-talk.config.yaml'),
join(root, 'config', 'sql-talk.config.yml'),
join(root, '.config', 'sql-talk.config.yaml'),
join(root, '.config', 'sql-talk.config.yml')
];
logger.info('🔍 Looking for configuration file...');
configCandidates.forEach((candidate, index) => {
try {
const stats = statSync(candidate);
if (stats.isFile()) {
logger.info(`✅ Found config file: ${candidate}`);
return candidate;
}
}
catch (error) {
logger.debug(` ${index + 1}. ${candidate} (not found)`);
}
});
// デフォルトの設定ファイルパスを返す
const defaultPath = join(root, 'sql-talk.config.yaml');
logger.warn(`⚠️ No config file found, using default: ${defaultPath}`);
return defaultPath;
}
/**
* ワークスペース情報を取得
* Get workspace information
*/
static getWorkspaceInfo() {
const root = this.detectProjectRoot();
const configPath = this.getConfigPath(root);
let hasConfig = false;
try {
const stats = statSync(configPath);
hasConfig = stats.isFile();
}
catch (error) {
// ファイルが存在しない
}
// プロジェクトタイプを判定
let projectType = 'unknown';
try {
const contents = readdirSync(root);
if (contents.includes('package.json')) {
projectType = 'node';
}
else if (contents.includes('requirements.txt') || contents.includes('pyproject.toml')) {
projectType = 'python';
}
else if (contents.includes('Cargo.toml')) {
projectType = 'rust';
}
else if (contents.includes('go.mod')) {
projectType = 'go';
}
}
catch (error) {
logger.debug(`Error detecting project type: ${error instanceof Error ? error.message : String(error)}`);
}
return {
root,
configPath,
hasConfig,
projectType
};
}
}
//# sourceMappingURL=workspace-detector.js.map