176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
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;
|