pilot-agent-cli
Version:
GitHub Copilot automation tool with configuration-driven file management
452 lines (388 loc) • 15.5 kB
JavaScript
/**
* 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;