@fkom13/mcp-sftp-orchestrator 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/sshPool.js ADDED
@@ -0,0 +1,272 @@
1
+ import { Client } from 'ssh2';
2
+ import fs from 'fs/promises';
3
+ import queue from './queue.js';
4
+
5
+ class SSHConnectionPool {
6
+ constructor() {
7
+ this.pools = new Map(); // Map<serverAlias, Connection[]>
8
+ this.activeConnections = new Map(); // Map<connectionId, {conn, serverAlias, inUse, lastUsed}>
9
+ this.config = {
10
+ maxConnections: 5, // Max connexions par serveur
11
+ minConnections: 1, // Min connexions maintenues
12
+ idleTimeout: 300000, // 5 minutes avant fermeture si inactif
13
+ keepAliveInterval: 30000, // Keep-alive toutes les 30s
14
+ connectionTimeout: 20000,
15
+ retryAttempts: 3
16
+ };
17
+
18
+ // Nettoyage périodique des connexions inactives
19
+ this.startCleanupInterval();
20
+ }
21
+
22
+ // Obtenir ou créer une connexion
23
+ async getConnection(serverAlias, serverConfig) {
24
+ // Chercher une connexion disponible
25
+ const pool = this.pools.get(serverAlias) || [];
26
+
27
+ for (const connId of pool) {
28
+ const connInfo = this.activeConnections.get(connId);
29
+ if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
30
+ connInfo.inUse = true;
31
+ connInfo.lastUsed = Date.now();
32
+ queue.log('info', `Réutilisation connexion SSH existante pour ${serverAlias}`);
33
+ return { id: connId, client: connInfo.conn };
34
+ }
35
+ }
36
+
37
+ // Si pas de connexion disponible, en créer une nouvelle
38
+ if (pool.length < this.config.maxConnections) {
39
+ const newConn = await this.createConnection(serverAlias, serverConfig);
40
+ return newConn;
41
+ }
42
+
43
+ // Si pool plein, attendre qu'une connexion se libère
44
+ queue.log('warn', `Pool SSH saturé pour ${serverAlias}, attente...`);
45
+ return await this.waitForConnection(serverAlias, serverConfig);
46
+ }
47
+
48
+ // Créer une nouvelle connexion
49
+ async createConnection(serverAlias, serverConfig) {
50
+ const conn = new Client();
51
+ const connId = `${serverAlias}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
52
+
53
+ return new Promise((resolve, reject) => {
54
+ let retries = 0;
55
+
56
+ const tryConnect = async () => {
57
+ try {
58
+ const config = {
59
+ host: serverConfig.host,
60
+ port: 22,
61
+ username: serverConfig.user,
62
+ readyTimeout: this.config.connectionTimeout,
63
+ keepaliveInterval: this.config.keepAliveInterval,
64
+ keepaliveCountMax: 3
65
+ };
66
+
67
+ if (serverConfig.keyPath) {
68
+ config.privateKey = await fs.readFile(serverConfig.keyPath);
69
+ } else if (serverConfig.password) {
70
+ config.password = serverConfig.password;
71
+ }
72
+
73
+ conn.on('ready', () => {
74
+ queue.log('info', `Nouvelle connexion SSH établie pour ${serverAlias}`);
75
+
76
+ // Ajouter au pool
77
+ if (!this.pools.has(serverAlias)) {
78
+ this.pools.set(serverAlias, []);
79
+ }
80
+ this.pools.get(serverAlias).push(connId);
81
+
82
+ // Enregistrer la connexion
83
+ this.activeConnections.set(connId, {
84
+ conn,
85
+ serverAlias,
86
+ inUse: true,
87
+ lastUsed: Date.now(),
88
+ config: serverConfig
89
+ });
90
+
91
+ // Ajouter un flag pour vérifier si la connexion est prête
92
+ conn.isReady = true;
93
+
94
+ resolve({ id: connId, client: conn });
95
+ });
96
+
97
+ conn.on('error', (err) => {
98
+ conn.isReady = false;
99
+ if (retries < this.config.retryAttempts) {
100
+ retries++;
101
+ queue.log('warn', `Tentative ${retries}/${this.config.retryAttempts} de connexion à ${serverAlias}`);
102
+ setTimeout(tryConnect, 2000 * retries);
103
+ } else {
104
+ this.removeConnection(connId);
105
+ reject(new Error(`Impossible de se connecter à ${serverAlias}: ${err.message}`));
106
+ }
107
+ });
108
+
109
+ conn.on('close', () => {
110
+ conn.isReady = false;
111
+ this.removeConnection(connId);
112
+ queue.log('info', `Connexion SSH fermée pour ${serverAlias}`);
113
+ });
114
+
115
+ conn.connect(config);
116
+ } catch (err) {
117
+ reject(err);
118
+ }
119
+ };
120
+
121
+ tryConnect();
122
+ });
123
+ }
124
+
125
+ // Libérer une connexion
126
+ releaseConnection(connId) {
127
+ const connInfo = this.activeConnections.get(connId);
128
+ if (connInfo) {
129
+ connInfo.inUse = false;
130
+ connInfo.lastUsed = Date.now();
131
+ queue.log('debug', `Connexion ${connId} libérée`);
132
+ }
133
+ }
134
+
135
+ // Fermer une connexion spécifique
136
+ closeConnection(connId) {
137
+ const connInfo = this.activeConnections.get(connId);
138
+ if (connInfo) {
139
+ try {
140
+ connInfo.conn.end();
141
+ } catch (e) {
142
+ // Ignorer les erreurs de fermeture
143
+ }
144
+ this.removeConnection(connId);
145
+ }
146
+ }
147
+
148
+ // Retirer une connexion du pool
149
+ removeConnection(connId) {
150
+ const connInfo = this.activeConnections.get(connId);
151
+ if (connInfo) {
152
+ const pool = this.pools.get(connInfo.serverAlias);
153
+ if (pool) {
154
+ const index = pool.indexOf(connId);
155
+ if (index > -1) {
156
+ pool.splice(index, 1);
157
+ }
158
+ if (pool.length === 0) {
159
+ this.pools.delete(connInfo.serverAlias);
160
+ }
161
+ }
162
+ this.activeConnections.delete(connId);
163
+ }
164
+ }
165
+
166
+ // Attendre qu'une connexion se libère
167
+ async waitForConnection(serverAlias, serverConfig, timeout = 30000) {
168
+ const startTime = Date.now();
169
+
170
+ return new Promise((resolve, reject) => {
171
+ const checkInterval = setInterval(async () => {
172
+ // Vérifier le timeout
173
+ if (Date.now() - startTime > timeout) {
174
+ clearInterval(checkInterval);
175
+ reject(new Error(`Timeout en attendant une connexion pour ${serverAlias}`));
176
+ return;
177
+ }
178
+
179
+ // Essayer d'obtenir une connexion
180
+ const pool = this.pools.get(serverAlias) || [];
181
+ for (const connId of pool) {
182
+ const connInfo = this.activeConnections.get(connId);
183
+ if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
184
+ clearInterval(checkInterval);
185
+ connInfo.inUse = true;
186
+ connInfo.lastUsed = Date.now();
187
+ resolve({ id: connId, client: connInfo.conn });
188
+ return;
189
+ }
190
+ }
191
+ }, 500);
192
+ });
193
+ }
194
+
195
+ // Nettoyer les connexions inactives
196
+ startCleanupInterval() {
197
+ setInterval(() => {
198
+ const now = Date.now();
199
+
200
+ for (const [connId, connInfo] of this.activeConnections) {
201
+ // Fermer les connexions inactives depuis trop longtemps
202
+ if (!connInfo.inUse && (now - connInfo.lastUsed) > this.config.idleTimeout) {
203
+ const pool = this.pools.get(connInfo.serverAlias) || [];
204
+
205
+ // Garder au moins minConnections
206
+ if (pool.length > this.config.minConnections) {
207
+ queue.log('info', `Fermeture connexion inactive: ${connId}`);
208
+ this.closeConnection(connId);
209
+ }
210
+ }
211
+
212
+ // Vérifier que la connexion est toujours vivante
213
+ if (!connInfo.conn.isReady && !connInfo.inUse) {
214
+ this.removeConnection(connId);
215
+ }
216
+ }
217
+ }, 60000); // Vérifier toutes les minutes
218
+ }
219
+
220
+ // Obtenir les statistiques du pool
221
+ getStats() {
222
+ const stats = {
223
+ totalConnections: this.activeConnections.size,
224
+ byServer: {}
225
+ };
226
+
227
+ for (const [serverAlias, pool] of this.pools) {
228
+ const connections = pool.map(connId => {
229
+ const info = this.activeConnections.get(connId);
230
+ return {
231
+ id: connId,
232
+ inUse: info?.inUse || false,
233
+ ready: info?.conn?.isReady || false,
234
+ lastUsed: info?.lastUsed
235
+ };
236
+ });
237
+
238
+ stats.byServer[serverAlias] = {
239
+ total: connections.length,
240
+ inUse: connections.filter(c => c.inUse).length,
241
+ available: connections.filter(c => !c.inUse && c.ready).length,
242
+ connections
243
+ };
244
+ }
245
+
246
+ return stats;
247
+ }
248
+
249
+ // Fermer toutes les connexions
250
+ closeAll() {
251
+ queue.log('info', 'Fermeture de toutes les connexions SSH...');
252
+ for (const connId of this.activeConnections.keys()) {
253
+ this.closeConnection(connId);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Instance singleton
259
+ const sshPool = new SSHConnectionPool();
260
+
261
+ // Nettoyer à la fermeture du processus
262
+ process.on('SIGINT', () => {
263
+ sshPool.closeAll();
264
+ process.exit(0);
265
+ });
266
+
267
+ process.on('SIGTERM', () => {
268
+ sshPool.closeAll();
269
+ process.exit(0);
270
+ });
271
+
272
+ export default sshPool;
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ import queue from './queue.js';
4
+ import sshPool from './sshPool.js';
5
+ import { glob } from 'glob';
6
+ import path from 'path';
7
+
8
+ console.log('🧪 Test des fonctionnalités MCP Orchestrator v6.0\n');
9
+
10
+ // Test 1: Queue persistante
11
+ console.log('✅ Test Queue:');
12
+ const testJob = queue.addJob({
13
+ type: 'test',
14
+ alias: 'test_server',
15
+ cmd: 'echo "Test"',
16
+ status: 'pending'
17
+ });
18
+ console.log(` - Tâche créée: ${testJob.id}`);
19
+ console.log(` - Stats queue:`, queue.getStats());
20
+
21
+ // Test 2: Pool de connexions
22
+ console.log('\n✅ Test Pool SSH:');
23
+ const poolStats = sshPool.getStats();
24
+ console.log(` - Connexions totales: ${poolStats.totalConnections}`);
25
+ console.log(` - Serveurs actifs: ${Object.keys(poolStats.byServer).length}`);
26
+
27
+ // Test 3: Patterns Glob
28
+ console.log('\n✅ Test Patterns Glob:');
29
+ async function testGlob() {
30
+ const patterns = ['*.js', '*.json'];
31
+ for (const pattern of patterns) {
32
+ const files = await glob(pattern, { cwd: process.cwd() });
33
+ console.log(` - Pattern "${pattern}": ${files.length} fichiers trouvés`);
34
+ }
35
+ }
36
+
37
+ // Test 4: Détection de prompts interactifs
38
+ console.log('\n✅ Test Détection Prompts:');
39
+ const testPrompts = [
40
+ 'Are you sure you want to continue? (yes/no)',
41
+ 'Password:',
42
+ 'Do you want to continue [Y/n]?',
43
+ 'Save configuration?'
44
+ ];
45
+
46
+ function detectPrompt(text) {
47
+ const patterns = {
48
+ 'continue': /continue.*\?/i,
49
+ 'password': /password:/i,
50
+ 'yes/no': /\(yes\/no\)/i,
51
+ 'y/n': /\[y\/n\]/i,
52
+ 'save': /save.*\?/i
53
+ };
54
+
55
+ for (const [name, regex] of Object.entries(patterns)) {
56
+ if (regex.test(text)) {
57
+ return name;
58
+ }
59
+ }
60
+ return null;
61
+ }
62
+
63
+ testPrompts.forEach(prompt => {
64
+ const detected = detectPrompt(prompt);
65
+ console.log(` - "${prompt.substring(0, 30)}..." → ${detected || 'non détecté'}`);
66
+ });
67
+
68
+ // Test 5: Logs système
69
+ console.log('\n✅ Test Logs:');
70
+ queue.log('info', 'Test log info');
71
+ queue.log('warn', 'Test log warning');
72
+ queue.log('error', 'Test log error');
73
+ const logs = queue.getLogs({ limit: 3 });
74
+ console.log(` - ${logs.length} logs récupérés`);
75
+
76
+ // Test 6: Gestion des tâches crashées
77
+ console.log('\n✅ Test Gestion Crashes:');
78
+ const crashedJob = queue.addJob({
79
+ type: 'test_crash',
80
+ alias: 'test',
81
+ status: 'running'
82
+ });
83
+ queue.updateJobStatus(crashedJob.id, 'crashed', {
84
+ canRetry: true,
85
+ crashedAt: new Date()
86
+ });
87
+ const crashed = queue.getCrashedJobs();
88
+ console.log(` - Tâches crashées: ${crashed.length}`);
89
+ if (crashed.length > 0) {
90
+ console.log(` - Peut être relancée: ${crashed[0].canRetry}`);
91
+ }
92
+
93
+ // Test 7: Statistiques
94
+ console.log('\n✅ Résumé des Statistiques:');
95
+ const finalStats = queue.getStats();
96
+ console.log(` - Total tâches: ${finalStats.total}`);
97
+ console.log(` - Par statut:`, finalStats.byStatus);
98
+ console.log(` - Par type:`, finalStats.byType);
99
+ console.log(` - Taux de succès: ${finalStats.successRate}%`);
100
+
101
+ // Exécuter les tests asynchrones
102
+ testGlob().then(() => {
103
+ console.log('\n✨ Tous les tests sont passés avec succès!');
104
+ console.log('📝 Note: Ce sont des tests unitaires, pas des tests de connexion réelle.');
105
+
106
+ // Nettoyer
107
+ queue.cleanOldJobs();
108
+ process.exit(0);
109
+ }).catch(err => {
110
+ console.error('❌ Erreur lors des tests:', err);
111
+ process.exit(1);
112
+ });