import * as path from 'path';
import * as fs from 'fs';
/**
* ARCHITECTURAL CONTEXT: Unified database configuration for all deployment methods
* PATTERN FOLLOWED: Single source of truth for database path resolution
* STRATEGIC PURPOSE: Eliminate configuration inconsistencies across Docker/NPX/Local
*/
export interface DatabaseConfig {
databaseUrl: string;
databasePath: string;
dataDirectory: string;
projectRoot: string;
deploymentMethod: 'docker' | 'npx' | 'local';
isProjectIsolated: boolean;
}
export interface DatabaseConfigOptions {
projectRoot?: string;
customDatabasePath?: string;
verbose?: boolean;
/**
* Skip environment-based project root detection (for testing)
*/
skipProjectRootDetection?: boolean;
}
export class DatabaseConfigManager {
private verbose: boolean;
constructor(options: DatabaseConfigOptions = {}) {
this.verbose = options.verbose || false;
}
/**
* Get unified database configuration for current deployment context
*/
getDatabaseConfig(options: DatabaseConfigOptions = {}): DatabaseConfig {
const deploymentMethod = this.detectDeploymentMethod();
const projectRoot = this.resolveProjectRoot(options.projectRoot);
switch (deploymentMethod) {
case 'docker':
return this.getDockerDatabaseConfig(projectRoot, options);
case 'npx':
return this.getNpxDatabaseConfig(projectRoot, options);
case 'local':
return this.getLocalDatabaseConfig(projectRoot, options);
default:
throw new Error(
`Unknown deployment method: ${deploymentMethod as any}`,
);
}
}
/**
* Detect current deployment method based on environment
*/
private detectDeploymentMethod(): 'docker' | 'npx' | 'local' {
// Docker detection: Check for Docker-specific environment variables or file system markers
if (
process.env.RUNNING_IN_DOCKER ||
fs.existsSync('/.dockerenv') ||
process.env.MIGRATIONS_PRE_DEPLOYED === 'true'
) {
return 'docker';
}
// NPX detection: Enhanced detection for VS Code extensions and various npx scenarios
const execPath = process.argv[1] || '';
const isNpxExecution =
execPath.includes('.npm/_npx') ||
execPath.includes('npm-cache/_npx') ||
execPath.includes('npm/global') ||
process.env.npm_execpath?.includes('npx') ||
process.env.npm_config_user_config?.includes('.npmrc') ||
// VS Code extension specific detection
process.env.VSCODE_PID !== undefined ||
process.env.TERM_PROGRAM === 'vscode' ||
// GitHub Copilot and other MCP clients
process.env.MCP_CLIENT_NAME !== undefined;
if (isNpxExecution) {
return 'npx';
}
// Local development: Default fallback
return 'local';
}
/**
* Resolve project root directory consistently
*/
private resolveProjectRoot(customRoot?: string): string {
// 1. Custom root provided (highest priority)
if (customRoot && path.isAbsolute(customRoot)) {
return customRoot;
}
// 2. PROJECT_ROOT environment variable
if (process.env.PROJECT_ROOT) {
return process.env.PROJECT_ROOT;
}
// 3. Current working directory (most common case)
return process.cwd();
}
/**
* Docker deployment configuration
* Pattern: /app/.anubis/workflow.db with volume mounting for project isolation
*/
private getDockerDatabaseConfig(
projectRoot: string,
_options: DatabaseConfigOptions,
): DatabaseConfig {
// Docker uses container-internal paths but supports volume mounting for project isolation
const dataDirectory = '/app/.anubis';
const databasePath = `${dataDirectory}/workflow.db`; // Always use Unix-style paths in Docker
const databaseUrl = `file:${databasePath}`;
return {
databaseUrl,
databasePath,
dataDirectory,
projectRoot,
deploymentMethod: 'docker',
isProjectIsolated: true, // Achieved via volume mounting
};
}
/**
* NPX deployment configuration
* Pattern: {projectRoot}/.anubis/workflow.db for automatic project isolation
*/
private getNpxDatabaseConfig(
projectRoot: string,
options: DatabaseConfigOptions,
): DatabaseConfig {
// Enhanced path resolution for VS Code extensions and MCP clients
let resolvedProjectRoot = projectRoot;
// Skip environment detection if explicitly requested (for testing)
if (!options.skipProjectRootDetection) {
// CRITICAL: Always respect PROJECT_ROOT environment variable from MCP configuration
if (
process.env.PROJECT_ROOT &&
path.isAbsolute(process.env.PROJECT_ROOT)
) {
resolvedProjectRoot = process.env.PROJECT_ROOT;
}
// If running in VS Code extension context, try to find workspace folder as fallback only
else if (
process.env.VSCODE_PID ||
process.env.TERM_PROGRAM === 'vscode'
) {
// Try to use workspace folder if available
const workspaceFolder =
process.env.VSCODE_WORKSPACE_FOLDER ||
process.env.PWD ||
process.cwd();
if (workspaceFolder && fs.existsSync(workspaceFolder)) {
resolvedProjectRoot = workspaceFolder;
}
}
}
// ALWAYS use project-specific .anubis directory - cross-platform path resolution
const dataDirectory = path.resolve(resolvedProjectRoot, '.anubis');
// Ensure we can create and write to the project data directory
try {
// Create directory if it doesn't exist
if (!fs.existsSync(dataDirectory)) {
fs.mkdirSync(dataDirectory, { recursive: true });
}
// Test write permissions with cross-platform approach
const testFile = path.join(dataDirectory, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
} catch (error) {
// NO FALLBACK - always fail with clear error message
const errorMessage = [
`β Cannot create data directory in project: ${resolvedProjectRoot}`,
` Target directory: ${dataDirectory}`,
` Error: ${error}`,
``,
`π‘ Possible solutions:`,
` 1. Check directory permissions for: ${resolvedProjectRoot}`,
` 2. Ensure the project directory is writable`,
` 3. Run with elevated permissions if needed`,
` 4. Check disk space availability`,
``,
`π§ Cross-platform note:`,
` - Windows: Ensure no antivirus blocking directory creation`,
` - Linux/Mac: Check user permissions with 'ls -la'`,
``,
`This tool requires project isolation and will NOT use system directories.`,
].join('\n');
throw new Error(errorMessage);
}
// Use cross-platform path resolution for database
const databasePath = path.resolve(dataDirectory, 'workflow.db');
const databaseUrl = `file:${databasePath}`;
return {
databaseUrl,
databasePath,
dataDirectory,
projectRoot: resolvedProjectRoot,
deploymentMethod: 'npx',
isProjectIsolated: true, // Always true - no fallbacks
};
}
/**
* Local development configuration
* Pattern: {projectRoot}/.anubis/workflow.db with optional custom paths
*/
private getLocalDatabaseConfig(
projectRoot: string,
options: DatabaseConfigOptions,
): DatabaseConfig {
// Support custom database path for local development flexibility
if (options.customDatabasePath) {
const customPath = path.resolve(projectRoot, options.customDatabasePath);
const dataDirectory = path.dirname(customPath);
const databaseUrl = `file:${customPath}`;
return {
databaseUrl,
databasePath: customPath,
dataDirectory,
projectRoot,
deploymentMethod: 'local',
isProjectIsolated: true,
};
}
// Default local development pattern
const dataDirectory = path.join(projectRoot, '.anubis');
const databasePath = path.join(dataDirectory, 'workflow.db');
const databaseUrl = `file:${databasePath}`;
return {
databaseUrl,
databasePath,
dataDirectory,
projectRoot,
deploymentMethod: 'local',
isProjectIsolated: true,
};
}
/**
* Ensure data directory exists with proper permissions
*/
ensureDataDirectory(config: DatabaseConfig): void {
if (!fs.existsSync(config.dataDirectory)) {
fs.mkdirSync(config.dataDirectory, { recursive: true });
}
}
/**
* Validate database configuration and fix common issues
*/
validateConfiguration(config: DatabaseConfig): {
isValid: boolean;
issues: string[];
} {
const issues: string[] = [];
// Check data directory accessibility
try {
if (!fs.existsSync(config.dataDirectory)) {
fs.mkdirSync(config.dataDirectory, { recursive: true });
}
// Test write permissions
const testFile = path.join(config.dataDirectory, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
} catch (error) {
issues.push(
`Data directory not writable: ${config.dataDirectory} - ${error}`,
);
}
// Check database URL format
if (!config.databaseUrl.startsWith('file:')) {
issues.push(
`Invalid database URL format: ${config.databaseUrl} (expected file: prefix)`,
);
}
// Check path consistency
const urlPath = config.databaseUrl.replace('file:', '');
if (urlPath !== config.databasePath) {
issues.push(
`Database URL and path mismatch: ${config.databaseUrl} vs ${config.databasePath}`,
);
}
return {
isValid: issues.length === 0,
issues,
};
}
/**
* Get environment variables for database configuration
*/
getEnvironmentVariables(config: DatabaseConfig): Record<string, string> {
return {
DATABASE_URL: config.databaseUrl,
PROJECT_ROOT: config.projectRoot,
MCP_DATA_DIRECTORY: config.dataDirectory,
MCP_DEPLOYMENT_METHOD: config.deploymentMethod,
};
}
/**
* Generate Docker volume mounting configuration
*/
getDockerVolumeConfig(config: DatabaseConfig): {
volumeMount: string;
containerPath: string;
hostPath: string;
} {
if (config.deploymentMethod !== 'docker') {
throw new Error(
'Docker volume config only available for Docker deployment',
);
}
const hostDataPath = path.join(config.projectRoot, '.anubis');
return {
volumeMount: `${hostDataPath}:/app/.anubis`,
containerPath: '/app/.anubis',
hostPath: hostDataPath,
};
}
}
/**
* Convenience function to get database configuration
*/
export function getDatabaseConfig(
options: DatabaseConfigOptions = {},
): DatabaseConfig {
const manager = new DatabaseConfigManager(options);
return manager.getDatabaseConfig(options);
}
/**
* Convenience function to setup database environment
*/
export function setupDatabaseEnvironment(
options: DatabaseConfigOptions = {},
): DatabaseConfig {
const manager = new DatabaseConfigManager(options);
const config = manager.getDatabaseConfig(options);
// Ensure data directory exists
manager.ensureDataDirectory(config);
// Validate configuration
const validation = manager.validateConfiguration(config);
if (!validation.isValid && process.env.NODE_ENV !== 'production') {
validation.issues.forEach((issue) => console.warn(` - ${issue}`));
}
// Set environment variables
const envVars = manager.getEnvironmentVariables(config);
Object.entries(envVars).forEach(([key, value]) => {
// CRITICAL: For NPX deployment, always override DATABASE_URL to ensure project isolation
if (key === 'DATABASE_URL' && config.deploymentMethod === 'npx') {
process.env[key] = value;
} else if (!process.env[key]) {
process.env[key] = value;
}
});
return config;
}