This commit is contained in:
2026-05-19 16:45:59 +02:00
parent 29496cb944
commit 992f25a9a2
16 changed files with 6222 additions and 116 deletions

175
src/config.ts Normal file
View File

@@ -0,0 +1,175 @@
import * as fs from 'fs';
import * as path from 'path';
/**
* Configuration with priority order:
* 1. Environment variables
* 2. .env file in project root
* 3. Interactive prompt (development fallback)
*/
export interface TokenConfig {
email: string;
token: string;
source: 'env' | 'dotenv' | 'prompt';
}
export interface DefaultConfig {
workspace: string | null;
repo: string | null;
}
/**
* Token configuration loader with priority order.
* Loads from environment variable, .env file, or prompts user.
*/
export class TokenConfigLoader {
private static readonly ENV_FILE = '.env';
private static readonly EMAIL_ENV = 'BITBUCKET_MCP_EMAIL';
private static readonly TOKEN_ENV = 'BITBUCKET_MCP_TOKEN';
/**
* Load configuration from configured sources with priority order.
* @returns TokenConfig with loaded email, token and source
*/
public static async load(): Promise<TokenConfig> {
// Priority 1: Environment variables (highest precedence)
const envEmail = process.env[this.EMAIL_ENV];
const envToken = process.env[this.TOKEN_ENV];
if (envEmail && envToken && this.isValidToken(envToken)) {
return { email: envEmail, token: envToken, source: 'env' };
}
// Debug: Log environment check result
console.debug(`[DEBUG] Env var check - email present: ${!!envEmail}, token present: ${!!envToken}, valid: ${!!(envEmail && envToken && this.isValidToken(envToken))}`);
// Priority 2: .env file in project root
const { email: dotenvEmail, token: dotenvToken } = this.loadFromDotEnv();
console.debug(`[DEBUG] Dotenv config loaded - email: ${!!dotenvEmail}, token: ${!!dotenvToken}`);
if (dotenvEmail && dotenvToken && this.isValidToken(dotenvToken)) {
return { email: dotenvEmail, token: dotenvToken, source: 'dotenv' };
}
// Debug: Log dotenv check result
console.debug(`[DEBUG] Dotenv check - email present: ${!!dotenvEmail}, token present: ${!!dotenvToken}, valid: ${!!(dotenvEmail && dotenvToken && this.isValidToken(dotenvToken))}`);
// Priority 3: Interactive prompt (fallback)
console.warn(
'⚠️ No Bitbucket credentials found. Please set one of:'
);
console.warn(' 1. Export BITBUCKET_MCP_EMAIL=<your_email> and BITBUCKET_MCP_TOKEN=<your_token>');
console.warn(' 2. Add to .env: BITBUCKET_MCP_EMAIL=<your_email> and BITBUCKET_MCP_TOKEN=<your_token>');
return this.promptForCredentials().then(credentials => {
if (credentials.email && this.isValidToken(credentials.token)) {
return { ...credentials, source: 'prompt' };
}
throw new Error(
'Invalid or missing Bitbucket credentials. Aborting.'
);
});
}
/**
* Load email and token from .env file in project root directory.
*/
private static loadFromDotEnv(): { email: string | null; token: string | null } {
console.debug('[DEBUG] Attempting to load credentials from .env file');
try {
// Resolve to project root (look for package.json)
let currentDir = process.cwd();
while (currentDir !== '/') {
const pkgPath = path.join(currentDir, 'package.json');
if (fs.existsSync(pkgPath)) break;
currentDir = path.dirname(currentDir);
}
const envPath = path.join(currentDir, this.ENV_FILE);
if (!fs.existsSync(envPath)) return { email: null, token: null };
const envContent = fs.readFileSync(envPath, 'utf-8');
const emailMatch = envContent.match(
new RegExp(`${this.EMAIL_ENV}\\s*=\\s*([^\\r\\n]+)`, 'i')
);
const tokenMatch = envContent.match(
new RegExp(`${this.TOKEN_ENV}\\s*=\\s*([^\\r\\n]+)`, 'i')
);
return {
email: emailMatch ? emailMatch[1].trim() : null,
token: tokenMatch ? tokenMatch[1].trim() : null
};
} catch (error) {
console.error('Failed to load .env:', error);
return { email: null, token: null };
}
}
/**
* Validate Bitbucket access token format.
*/
private static isValidToken(token: string): boolean {
if (!token || token.length < 8) return false;
const validPattern = /^[A-Za-z0-9=._\-+/]+$/;
return validPattern.test(token);
}
/**
* Prompt user for credentials (development fallback).
*/
private static async promptForCredentials(): Promise<{ email: string; token: string }> {
// Skip prompting if running in non-Node.js environment
if (typeof require === 'undefined') {
return { email: '', token: '' };
}
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askQuestion = (question: string): Promise<string> => {
return new Promise((resolve) => {
rl.question(question, (answer: string) => {
resolve(answer);
});
});
};
return new Promise((resolve) => {
rl.question(
'Enter your Bitbucket email (or press Enter to skip): ',
async (email: string) => {
const token = await askQuestion('Enter your Bitbucket access token (or press Enter to skip): ');
rl.close();
resolve({ email: email || '', token: token || '' });
}
);
});
}
}
export class DefaultConfigLoader {
private static readonly WORKSPACE_ENV = 'DEFAULT_WORKSPACE';
private static readonly REPO_ENV = 'DEFAULT_REPO';
public static load(): DefaultConfig {
return {
workspace: process.env[this.WORKSPACE_ENV] || null,
repo: process.env[this.REPO_ENV] || null
};
}
public static getWorkspace(): string | null {
return process.env[this.WORKSPACE_ENV] || null;
}
public static getRepo(): string | null {
return process.env[this.REPO_ENV] || null;
}
}
export default TokenConfigLoader;