UNPKG

pilot-agent-cli

Version:

GitHub Copilot automation tool with configuration-driven file management

452 lines (388 loc) 15.5 kB
/** * CopilotAgentInitializer - Service pour initialiser correctement l'agent Copilot * Suit les principes SOLID et l'architecture hexagonale */ const { EventEmitter } = require('events'); const path = require('path'); class CopilotAgentInitializer extends EventEmitter { constructor(logger = console) { super(); this.logger = logger; this.isInitialized = false; this.initializationPromise = null; this.messageId = 1; this.pendingRequests = new Map(); this.serverCapabilities = null; this.agentStatus = 'unknown'; // 'unknown', 'initializing', 'ready', 'authenticated', 'error' // Ensure path module is available as instance property this.pathUtils = path; } /** * Initialise l'agent service avec la séquence LSP correcte * @param {ChildProcess} serverProcess - Processus du serveur Copilot * @returns {Promise<boolean>} - True si l'initialisation réussit */ async initializeAgent(serverProcess) { if (this.isInitialized && this.agentStatus === 'authenticated') { this.logger.log('✅ Agent déjà initialisé et authentifié'); return true; } if (this.initializationPromise) { this.logger.log('⏳ Initialisation déjà en cours...'); return this.initializationPromise; } this.initializationPromise = this._performInitialization(serverProcess); return this.initializationPromise; } /** * Effectue la séquence d'initialisation complète * @private */ async _performInitialization(serverProcess) { try { this.agentStatus = 'initializing'; this.emit('statusChanged', 'initializing'); // 1. Configuration du gestionnaire de messages this._setupMessageHandler(serverProcess); // 2. Initialisation LSP const initResult = await this._sendInitializeRequest(serverProcess); if (!initResult) { throw new Error('Échec de l\'initialisation LSP'); } // 3. Notification initialized await this._sendInitializedNotification(serverProcess); // 4. Configuration de l'agent await this._configureAgent(serverProcess); // 5. Vérification du statut d'authentification const authStatus = await this._checkAuthenticationStatus(serverProcess); this.isInitialized = true; this.agentStatus = authStatus ? 'authenticated' : 'ready'; this.emit('statusChanged', this.agentStatus); this.logger.log(`✅ Agent initialisé avec succès - Statut: ${this.agentStatus}`); return true; } catch (error) { this.agentStatus = 'error'; this.emit('statusChanged', 'error'); this.emit('error', error); this.logger.error(`❌ Échec de l'initialisation de l'agent: ${error.message}`); throw error; } } /** * Configure le gestionnaire de messages LSP * @private */ _setupMessageHandler(serverProcess) { let buffer = ''; serverProcess.stdout.on('data', (data) => { buffer += data.toString(); while (true) { const headerEnd = buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) break; const header = buffer.substring(0, headerEnd); const contentLengthMatch = header.match(/Content-Length: (\d+)/); if (!contentLengthMatch) break; const contentLength = parseInt(contentLengthMatch[1]); const messageStart = headerEnd + 4; const messageEnd = messageStart + contentLength; if (buffer.length < messageEnd) break; const messageContent = buffer.substring(messageStart, messageEnd); buffer = buffer.substring(messageEnd); try { const message = JSON.parse(messageContent); this._handleMessage(message); } catch (parseError) { this.logger.error('Erreur de parsing JSON:', parseError.message); } } }); serverProcess.stderr.on('data', (data) => { const errorMessage = data.toString(); if (!errorMessage.includes('experimental') && !errorMessage.includes('warning') && !errorMessage.includes('DEP0132')) { this.logger.error('Erreur serveur:', errorMessage); } }); } /** * Gère les messages reçus du serveur * @private */ _handleMessage(message) { // Gérer les réponses aux requêtes if (message.id && this.pendingRequests.has(message.id)) { const { resolve, reject, method } = this.pendingRequests.get(message.id); this.pendingRequests.delete(message.id); if (message.error) { reject(new Error(`${method}: ${message.error.message}`)); } else { resolve(message.result); } return; } // Gérer les notifications du serveur if (message.method) { this.emit('notification', message.method, message.params); } // Log pour débogage if (this.logger.debug) { this.logger.debug('Message reçu:', JSON.stringify(message, null, 2)); } } /** * Envoie la requête d'initialisation LSP * @private */ async _sendInitializeRequest(serverProcess) { // Get current working directory safely const currentDir = process.cwd(); const rootUri = `file://${currentDir.replace(/\\/g, '/')}`; const workspaceName = this.pathUtils.basename(currentDir); const initializeParams = { processId: process.pid, rootUri: rootUri, capabilities: { textDocument: { completion: { completionItem: { snippetSupport: true, commitCharactersSupport: true, documentationFormat: ['markdown', 'plaintext'], resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits'] } }, contextSupport: true, dynamicRegistration: true }, signatureHelp: { signatureInformation: { documentationFormat: ['markdown', 'plaintext'] } } }, workspace: { configuration: true, workspaceFolders: true, applyEdit: true } }, initializationOptions: { editorInfo: { name: "pilot-agent-cli", version: "1.2.5" }, editorPluginInfo: { name: "pilot-agent-plugin", version: "1.2.5" }, // Options spécifiques à Copilot enableAutoCompletions: true, enableSuggestions: true }, workspaceFolders: [{ uri: rootUri, name: workspaceName }] }; try { const result = await this._sendRequest(serverProcess, 'initialize', initializeParams); this.serverCapabilities = result.capabilities; this.logger.log('✅ Serveur LSP initialisé'); return true; } catch (error) { this.logger.error('❌ Échec de l\'initialisation LSP:', error.message); return false; } } /** * Envoie la notification initialized * @private */ async _sendInitializedNotification(serverProcess) { this._sendNotification(serverProcess, 'initialized', {}); this.logger.log('✅ Notification initialized envoyée'); // Attendre un peu pour que le serveur traite la notification await new Promise(resolve => setTimeout(resolve, 1000)); } /** * Configure l'agent avec les paramètres nécessaires * @private */ async _configureAgent(serverProcess) { try { const currentDir = process.cwd(); const rootUri = `file://${currentDir.replace(/\\/g, '/')}`; const workspaceName = this.pathUtils.basename(currentDir); // Configuration de l'espace de travail si supportée if (this.serverCapabilities?.workspace?.configuration) { await this._sendRequest(serverProcess, 'workspace/didChangeConfiguration', { settings: { copilot: { enable: true, suggestions: { enable: true } } } }); this.logger.log('✅ Configuration de l\'espace de travail envoyée'); } // Notification d'ouverture du dossier de travail if (this.serverCapabilities?.workspace?.workspaceFolders) { this._sendNotification(serverProcess, 'workspace/didChangeWorkspaceFolders', { event: { added: [{ uri: rootUri, name: workspaceName }], removed: [] } }); this.logger.log('✅ Notification workspace folders envoyée'); } } catch (error) { this.logger.warn('⚠️ Configuration optionnelle échouée:', error.message); } } /** * Vérifie le statut d'authentification * @private */ async _checkAuthenticationStatus(serverProcess) { try { // Essayer différentes méthodes pour vérifier l'authentification const statusMethods = ['checkStatus', 'getStatus', 'status']; for (const method of statusMethods) { try { const result = await this._sendRequest(serverProcess, method, {}, 3000); if (result && (result.status === 'OK' || result.user)) { this.logger.log(`✅ Authentification confirmée via ${method}`); return true; } } catch (error) { // Continuer avec la méthode suivante continue; } } this.logger.log('ℹ️ Statut d\'authentification non confirmé'); return false; } catch (error) { this.logger.warn('⚠️ Impossible de vérifier le statut d\'authentification:', error.message); return false; } } /** * Envoie une requête LSP et attend la réponse * @private */ _sendRequest(serverProcess, method, params = {}, timeout = 10000) { return new Promise((resolve, reject) => { const id = this.messageId++; const message = { jsonrpc: '2.0', id, method, params }; // Stocker la promesse pour la résolution this.pendingRequests.set(id, { resolve, reject, method }); // Timeout const timeoutId = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Timeout pour la requête ${method}`)); }, timeout); // Nettoyer le timeout si la requête se termine const originalResolve = resolve; const originalReject = reject; const wrappedResolve = (result) => { clearTimeout(timeoutId); originalResolve(result); }; const wrappedReject = (error) => { clearTimeout(timeoutId); originalReject(error); }; this.pendingRequests.set(id, { resolve: wrappedResolve, reject: wrappedReject, method }); // Envoyer le message this._writeMessage(serverProcess, message); }); } /** * Envoie une notification LSP (sans attendre de réponse) * @private */ _sendNotification(serverProcess, method, params = {}) { const message = { jsonrpc: '2.0', method, params }; this._writeMessage(serverProcess, message); } /** * Écrit un message au serveur avec le protocole LSP * @private */ _writeMessage(serverProcess, message) { const content = JSON.stringify(message); const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`; try { serverProcess.stdin.write(header + content); } catch (error) { this.logger.error('Erreur d\'écriture vers le serveur:', error.message); throw error; } } /** * Déclenche l'authentification si nécessaire */ async triggerAuthentication(serverProcess) { if (this.agentStatus === 'authenticated') { this.logger.log('✅ Déjà authentifié'); return true; } try { this.logger.log('🔐 Démarrage de l\'authentification...'); const result = await this._sendRequest(serverProcess, 'signInInitiate', {}); if (result && (result.verificationUri || result.userCode)) { this.emit('authenticationRequired', result); return result; } this.logger.log('✅ Authentification déjà active'); return true; } catch (error) { if (error.message.includes('No pending sign in')) { this.logger.log('✅ Déjà authentifié (pas de connexion en attente)'); this.agentStatus = 'authenticated'; return true; } throw error; } } /** * Obtient le statut actuel de l'agent */ getStatus() { return { isInitialized: this.isInitialized, agentStatus: this.agentStatus, capabilities: this.serverCapabilities }; } /** * Nettoie les ressources */ cleanup() { this.pendingRequests.clear(); this.removeAllListeners(); this.isInitialized = false; this.agentStatus = 'unknown'; } } module.exports = CopilotAgentInitializer;