/**
* Provider Operation Handler
*
* Unified interface for handling operations across multiple providers.
* Supports provider="both" with parallel execution, result aggregation,
* and partial failure handling.
*/
import { BaseProvider, MultiProvider } from './base-provider.js';
import { ProviderFactory, ProviderType } from './provider-factory.js';
import { ProviderConfig, ProviderResult, ProviderOperation } from './types.js';
import { configManager } from '../config.js';
import { RepositorySynchronizer } from '../utils/repository-sync.js';
import { DataMerger } from '../utils/data-merger.js';
export interface OperationResult {
success: boolean;
results: ProviderResult[];
partialFailure: boolean;
errors: ProviderResult[];
data?: any; // UNIVERSAL MODE: Dados mesclados para operações de leitura
metadata: {
operation: string;
provider: string;
executionTime: number;
timestamp: string;
};
}
export class ProviderOperationHandler {
private factory: ProviderFactory;
private config: ProviderConfig;
private repositorySync: RepositorySynchronizer;
constructor(config: ProviderConfig) {
this.config = config;
this.factory = new ProviderFactory(config);
this.repositorySync = new RepositorySynchronizer(config);
}
/**
* Execute operation with unified interface
*/
async executeOperation(operation: ProviderOperation): Promise<OperationResult> {
const startTime = Date.now();
try {
// UNIVERSAL MODE: Auto-aplicar provider="both" se modo universal ativo
if (configManager.isUniversalMode() && operation.provider !== 'both') {
operation.provider = 'both';
console.error('[Universal Mode] Auto-applying both providers');
}
// UNIVERSAL MODE: Sincronização pré-operação para operações de escrita
if (configManager.isUniversalMode() && this.isWriteOperation(operation.operation)) {
await this.ensureRepositorySynchronized(operation);
}
const provider = this.factory.createProviderFromString(operation.provider);
let result: OperationResult;
if (this.isMultiProvider(provider)) {
result = await this.executeMultiProviderOperation(provider, operation, startTime);
// UNIVERSAL MODE: Mesclagem pós-operação para operações de leitura
if (configManager.isUniversalMode() && this.isReadOperation(operation.operation)) {
result.data = this.mergeOperationResults(operation.operation, result.results);
}
// UNIVERSAL MODE: Tentativa de recuperação em falha parcial
if (result.partialFailure && configManager.isUniversalMode()) {
await this.attemptAutoRecovery(operation, result);
}
} else {
result = await this.executeSingleProviderOperation(provider, operation, startTime);
}
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.formatOperationError(operation, startTime, errorMessage, error);
}
}
/**
* Execute operation on single provider
*/
private async executeSingleProviderOperation(
provider: BaseProvider,
operation: ProviderOperation,
startTime: number
): Promise<OperationResult> {
try {
// Auto-fill repo from projectPath when missing
if (operation.parameters && !operation.parameters.repo && operation.parameters.projectPath) {
try {
const { repositoryDetector } = await import('../utils/repository-detector.js');
const context = await repositoryDetector.getProjectContext(operation.parameters.projectPath);
if (context && context.autoDetectedSettings) {
if (!operation.parameters.repo && context.autoDetectedSettings.repository) {
operation.parameters.repo = context.autoDetectedSettings.repository;
}
}
} catch {
// ignore auto-detection errors and proceed, provider will return meaningful error
}
}
const result = await provider.executeOperation(operation.operation, operation.parameters);
return {
success: result.success,
results: [result],
partialFailure: false,
errors: result.success ? [] : [result],
metadata: {
operation: operation.operation,
provider: operation.provider,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorResult: ProviderResult = {
success: false,
error: {
code: 'PROVIDER_EXECUTION_ERROR',
message: errorMessage,
details: error
},
provider: operation.provider
};
return {
success: false,
results: [],
partialFailure: false,
errors: [errorResult],
metadata: {
operation: operation.operation,
provider: operation.provider,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
}
/**
* Execute operation on multiple providers in parallel
*/
private async executeMultiProviderOperation(
multiProvider: MultiProvider,
operation: ProviderOperation,
startTime: number
): Promise<OperationResult> {
try {
const results = await multiProvider.executeOperation(operation.operation, operation.parameters);
const successResults = results.filter(r => r.success);
const errorResults = results.filter(r => !r.success);
const hasSuccess = successResults.length > 0;
const hasErrors = errorResults.length > 0;
const partialFailure = hasSuccess && hasErrors;
return {
success: hasSuccess,
results: successResults,
partialFailure,
errors: errorResults,
metadata: {
operation: operation.operation,
provider: operation.provider,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.formatOperationError(operation, startTime, errorMessage, error);
}
}
/**
* Validate operation before execution
*/
validateOperation(operation: ProviderOperation): {
valid: boolean;
errors: string[];
warnings: string[];
} {
const errors: string[] = [];
const warnings: string[] = [];
// Validate provider
if (!operation.provider) {
errors.push('Provider is required');
} else if (!['github', 'gitea', 'both'].includes(operation.provider)) {
errors.push(`Invalid provider: ${operation.provider}. Valid options: github, gitea, both`);
}
// Validate operation name
if (!operation.operation) {
errors.push('Operation is required');
}
// Validate parameters
if (!operation.parameters) {
errors.push('Parameters are required');
}
// Check provider availability
if (operation.provider === 'github' && !this.factory.isProviderAvailable('github')) {
errors.push('GitHub provider is not configured');
}
if (operation.provider === 'gitea' && !this.factory.isProviderAvailable('gitea')) {
errors.push('Gitea provider is not configured');
}
if (operation.provider === 'both') {
const availableProviders = this.factory.getAvailableProviders();
if (availableProviders.length === 0) {
errors.push('No providers are configured');
} else if (availableProviders.length === 1) {
warnings.push(`Only ${availableProviders[0]} provider is configured, 'both' will only use one provider`);
}
}
// Validate authentication requirements
if (operation.requiresAuth) {
if (operation.provider === 'github' && !this.config.github?.token) {
errors.push('GitHub authentication is required for this operation');
}
if (operation.provider === 'gitea' && !this.config.gitea?.token) {
errors.push('Gitea authentication is required for this operation');
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Get provider status and configuration
*/
getProviderStatus(): {
github: {
configured: boolean;
available: boolean;
missingFields: string[];
};
gitea: {
configured: boolean;
available: boolean;
missingFields: string[];
};
multiProvider: {
available: boolean;
configuredProviders: string[];
};
} {
const githubValidation = this.factory.validateProviderConfig('github');
const giteaValidation = this.factory.validateProviderConfig('gitea');
const availableProviders = this.factory.getAvailableProviders();
return {
github: {
configured: githubValidation.valid,
available: this.factory.isProviderAvailable('github'),
missingFields: githubValidation.missingFields
},
gitea: {
configured: giteaValidation.valid,
available: this.factory.isProviderAvailable('gitea'),
missingFields: giteaValidation.missingFields
},
multiProvider: {
available: availableProviders.length > 1,
configuredProviders: availableProviders
}
};
}
/**
* Aggregate results from multiple providers
*/
aggregateResults(results: ProviderResult[]): {
combinedData: any;
summary: {
totalProviders: number;
successfulProviders: number;
failedProviders: number;
providers: string[];
};
} {
const successResults = results.filter(r => r.success);
const failedResults = results.filter(r => !r.success);
// Combine data from successful results
let combinedData: any = {};
if (successResults.length === 1) {
combinedData = successResults[0].data;
} else if (successResults.length > 1) {
// For multiple results, create an object with provider-specific data
combinedData = {};
successResults.forEach(result => {
combinedData[result.provider] = result.data;
});
}
return {
combinedData,
summary: {
totalProviders: results.length,
successfulProviders: successResults.length,
failedProviders: failedResults.length,
providers: results.map(r => r.provider)
}
};
}
/**
* Handle partial failures with retry logic
*/
async handlePartialFailure(
operation: ProviderOperation,
results: ProviderResult[],
retryOptions?: {
maxRetries: number;
retryDelay: number;
retryFailedOnly: boolean;
}
): Promise<OperationResult> {
const options = {
maxRetries: 1,
retryDelay: 1000,
retryFailedOnly: true,
...retryOptions
};
const failedResults = results.filter(r => !r.success);
if (failedResults.length === 0 || options.maxRetries === 0) {
// No failures or no retries requested
return this.executeOperation(operation);
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, options.retryDelay));
if (options.retryFailedOnly) {
// Retry only failed providers
const failedProviders = failedResults.map(r => r.provider);
const retryOperation: ProviderOperation = {
...operation,
provider: failedProviders.length === 1 ? failedProviders[0] as any : 'both'
};
return this.executeOperation(retryOperation);
} else {
// Retry entire operation
return this.executeOperation(operation);
}
}
/**
* Check if provider is multi-provider
*/
private isMultiProvider(provider: BaseProvider | MultiProvider): provider is MultiProvider {
return 'providers' in provider;
}
/**
* Check if operation is a write operation
*/
private isWriteOperation(operation: string): boolean {
const writeOps = ['create', 'update', 'delete', 'merge', 'close', 'publish'];
return writeOps.some(op => operation.includes(op));
}
/**
* Check if operation is a read operation
*/
private isReadOperation(operation: string): boolean {
const readOps = ['list', 'get', 'search', 'show'];
return readOps.some(op => operation.includes(op));
}
/**
* Ensure repository is synchronized before write operations
*/
private async ensureRepositorySynchronized(operation: ProviderOperation): Promise<void> {
if (operation.parameters.repo) {
if (operation.provider === 'both') {
// For 'both' provider, ensure repository exists on both GitHub and Gitea
await Promise.all([
this.repositorySync.ensureRepositoryExists(operation.parameters.repo, 'github'),
this.repositorySync.ensureRepositoryExists(operation.parameters.repo, 'gitea')
]);
} else {
await this.repositorySync.ensureRepositoryExists(
operation.parameters.repo,
operation.provider
);
}
}
}
/**
* Merge operation results for read operations
*/
private mergeOperationResults(operation: string, results: ProviderResult[]): any {
if (operation.includes('list')) {
return DataMerger.mergeListResults(results);
} else if (operation.includes('get')) {
return DataMerger.mergeGetResults(results);
} else if (operation.includes('search')) {
return DataMerger.mergeSearchResults(results);
}
return results;
}
/**
* Attempt automatic recovery for partial failures
*/
private async attemptAutoRecovery(
operation: ProviderOperation,
result: OperationResult
): Promise<void> {
for (const error of result.errors) {
const recovered = await this.repositorySync.attemptRecovery(
operation.operation,
operation.parameters,
error.provider
);
if (recovered) {
console.error(`[Universal Mode] Auto-recovered ${error.provider}`);
}
}
}
/**
* Format operation error
*/
private formatOperationError(
operation: ProviderOperation,
startTime: number,
message: string,
details?: any
): OperationResult {
const errorResult: ProviderResult = {
success: false,
error: {
code: 'OPERATION_ERROR',
message,
details
},
provider: operation.provider
};
return {
success: false,
results: [],
partialFailure: false,
errors: [errorResult],
metadata: {
operation: operation.operation,
provider: operation.provider,
executionTime: Date.now() - startTime,
timestamp: new Date().toISOString()
}
};
}
}