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 { // 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= and BITBUCKET_MCP_TOKEN='); console.warn(' 2. Add to .env: BITBUCKET_MCP_EMAIL= and BITBUCKET_MCP_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 => { 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;