@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.3.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/.env.example +21 -15
- package/.gencodedoc.yaml +76 -0
- package/README.fr.md +369 -0
- package/README.md +333 -61
- package/ROADMAP.md +21 -0
- package/apis.js +17 -0
- package/compareEngine.js +132 -0
- package/config.js +35 -4
- package/diagnose.js +89 -0
- package/diffEngine.js +141 -0
- package/diffFormatter.js +114 -0
- package/fileOps.js +275 -0
- package/guide.js +236 -0
- package/history.js +8 -1
- package/notes.js +87 -0
- package/package.json +5 -22
- package/policies.js +76 -0
- package/queue.js +107 -121
- package/server.js +1460 -108
- package/sftp.js +131 -41
- package/shellSessions.js +237 -0
- package/snapshotManager.js +324 -0
- package/sourceAdapter.js +357 -0
- package/ssh.js +168 -84
- package/sshPool.js +52 -56
- package/test_mcp.js +55 -0
- package/tunnels.js +224 -0
- package/utils.js +6 -0
- package/publish1github.md +0 -216
package/queue.js
CHANGED
|
@@ -5,8 +5,11 @@ import config from './config.js';
|
|
|
5
5
|
|
|
6
6
|
const QUEUE_FILE = path.join(config.dataDir, 'queue.json');
|
|
7
7
|
const QUEUE_BACKUP = path.join(config.dataDir, 'queue.backup.json');
|
|
8
|
-
const SAVE_INTERVAL = 5000;
|
|
9
|
-
const MAX_QUEUE_SIZE = 1000;
|
|
8
|
+
const SAVE_INTERVAL = 5000;
|
|
9
|
+
const MAX_QUEUE_SIZE = 1000;
|
|
10
|
+
|
|
11
|
+
// ✅ Mode silencieux par défaut (logs désactivés sauf si MCP_DEBUG=true)
|
|
12
|
+
const SILENT_MODE = process.env.MCP_DEBUG !== 'true';
|
|
10
13
|
|
|
11
14
|
const jobQueue = {};
|
|
12
15
|
const logHistory = [];
|
|
@@ -20,31 +23,27 @@ async function loadQueue() {
|
|
|
20
23
|
try {
|
|
21
24
|
const data = await fs.readFile(QUEUE_FILE, 'utf-8');
|
|
22
25
|
const savedQueue = JSON.parse(data);
|
|
23
|
-
|
|
24
|
-
// Restaurer les tâches non terminées
|
|
26
|
+
|
|
25
27
|
for (const [id, job] of Object.entries(savedQueue)) {
|
|
26
|
-
// Convertir les dates string en objets Date
|
|
27
28
|
if (job.createdAt) job.createdAt = new Date(job.createdAt);
|
|
28
29
|
if (job.updatedAt) job.updatedAt = new Date(job.updatedAt);
|
|
29
30
|
if (job.reminderAt) job.reminderAt = new Date(job.reminderAt);
|
|
30
|
-
|
|
31
|
-
// Marquer les tâches running comme crashed
|
|
31
|
+
|
|
32
32
|
if (job.status === 'running') {
|
|
33
33
|
job.status = 'crashed';
|
|
34
34
|
job.crashedAt = new Date();
|
|
35
35
|
job.canRetry = true;
|
|
36
36
|
log('warn', `Tâche ${id} marquée comme crashed (reprise après redémarrage)`);
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
jobQueue[id] = job;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
log('info', `${Object.keys(jobQueue).length} tâches restaurées depuis la sauvegarde`);
|
|
43
43
|
} catch (err) {
|
|
44
44
|
if (err.code !== 'ENOENT') {
|
|
45
45
|
log('error', `Erreur lors du chargement de la queue: ${err.message}`);
|
|
46
|
-
|
|
47
|
-
// Essayer de charger la sauvegarde de secours
|
|
46
|
+
|
|
48
47
|
try {
|
|
49
48
|
const backupData = await fs.readFile(QUEUE_BACKUP, 'utf-8');
|
|
50
49
|
const backupQueue = JSON.parse(backupData);
|
|
@@ -59,58 +58,62 @@ async function loadQueue() {
|
|
|
59
58
|
|
|
60
59
|
let isSaving = false;
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
let saveLock = null;
|
|
62
|
+
|
|
63
63
|
async function saveQueue() {
|
|
64
|
-
if (!isDirty
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
if (!isDirty) return;
|
|
65
|
+
|
|
66
|
+
// Attendre le verrou précédent
|
|
67
|
+
if (saveLock) await saveLock;
|
|
68
|
+
|
|
69
|
+
saveLock = (async () => {
|
|
70
|
+
isSaving = true;
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Filtrer les tâches terminées anciennes (garder 24h)
|
|
76
|
-
const now = Date.now();
|
|
77
|
-
const filteredQueue = {};
|
|
78
|
-
|
|
79
|
-
for (const [id, job] of Object.entries(jobQueue)) {
|
|
80
|
-
const age = now - new Date(job.createdAt).getTime();
|
|
81
|
-
const isRecent = age < 86400000; // 24 heures
|
|
82
|
-
const isActive = ['pending', 'running', 'crashed'].includes(job.status);
|
|
83
|
-
|
|
84
|
-
if (isActive || isRecent) {
|
|
85
|
-
filteredQueue[id] = job;
|
|
72
|
+
try {
|
|
73
|
+
await fs.copyFile(QUEUE_FILE, QUEUE_BACKUP);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Ignorer si le fichier n'existe pas
|
|
86
76
|
}
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const filteredQueue = {};
|
|
80
|
+
|
|
81
|
+
for (const [id, job] of Object.entries(jobQueue)) {
|
|
82
|
+
const age = now - new Date(job.createdAt).getTime();
|
|
83
|
+
const isRecent = age < 86400000;
|
|
84
|
+
const isActive = ['pending', 'running', 'crashed'].includes(job.status);
|
|
85
|
+
|
|
86
|
+
if (isActive || isRecent) {
|
|
87
|
+
filteredQueue[id] = job;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fs.writeFile(
|
|
92
|
+
QUEUE_FILE,
|
|
93
|
+
JSON.stringify(filteredQueue, null, 2)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
isDirty = false;
|
|
97
|
+
log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
|
|
100
|
+
} finally {
|
|
101
|
+
isSaving = false;
|
|
102
|
+
saveLock = null;
|
|
87
103
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
QUEUE_FILE,
|
|
92
|
-
JSON.stringify(filteredQueue, null, 2)
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
isDirty = false;
|
|
96
|
-
log('debug', `Queue sauvegardée (${Object.keys(filteredQueue).length} tâches)`);
|
|
97
|
-
} catch (err) {
|
|
98
|
-
log('error', `Erreur lors de la sauvegarde de la queue: ${err.message}`);
|
|
99
|
-
} finally {
|
|
100
|
-
isSaving = false;
|
|
101
|
-
}
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
return saveLock;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
// Démarrer la sauvegarde automatique
|
|
105
109
|
function startAutoSave() {
|
|
106
110
|
if (saveTimer) clearInterval(saveTimer);
|
|
107
|
-
|
|
111
|
+
|
|
108
112
|
saveTimer = setInterval(() => {
|
|
109
113
|
saveQueue();
|
|
110
114
|
}, SAVE_INTERVAL);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
// Arrêter la sauvegarde automatique
|
|
114
117
|
function stopAutoSave() {
|
|
115
118
|
if (saveTimer) {
|
|
116
119
|
clearInterval(saveTimer);
|
|
@@ -124,31 +127,30 @@ function log(level, message) {
|
|
|
124
127
|
if (logHistory.length > MAX_LOGS) {
|
|
125
128
|
logHistory.shift();
|
|
126
129
|
}
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
if (['error', 'warn', 'info'].includes(level)) {
|
|
130
|
+
|
|
131
|
+
// ✅ N'afficher les logs que si MCP_DEBUG=true
|
|
132
|
+
if (!SILENT_MODE && ['error', 'warn', 'info'].includes(level)) {
|
|
130
133
|
const prefix = {
|
|
131
134
|
error: '[❌ ERROR]',
|
|
132
135
|
warn: '[⚠️ WARN]',
|
|
133
136
|
info: '[ℹ️ INFO]',
|
|
134
137
|
debug: '[🔧 DEBUG]'
|
|
135
138
|
}[level] || `[${level.toUpperCase()}]`;
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
|
|
140
|
+
// ✅ TOUJOURS utiliser stderr (pas stdout)
|
|
141
|
+
console.error(`${prefix} ${new Date().toISOString().split('T')[1].split('.')[0]} - ${message}`);
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
function addJob(details) {
|
|
142
|
-
// Vérifier la taille de la queue
|
|
143
146
|
if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
|
|
144
|
-
// Nettoyer les vieilles tâches terminées
|
|
145
147
|
cleanOldJobs();
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
if (Object.keys(jobQueue).length >= MAX_QUEUE_SIZE) {
|
|
148
150
|
throw new Error(`Queue pleine (${MAX_QUEUE_SIZE} tâches max)`);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
|
-
|
|
153
|
+
|
|
152
154
|
const id = uuidv4().split('-')[0];
|
|
153
155
|
const job = {
|
|
154
156
|
id,
|
|
@@ -174,8 +176,7 @@ function updateJobStatus(id, status, data = {}) {
|
|
|
174
176
|
const oldStatus = jobQueue[id].status;
|
|
175
177
|
jobQueue[id].status = status;
|
|
176
178
|
jobQueue[id].updatedAt = new Date();
|
|
177
|
-
|
|
178
|
-
// Copier toutes les données supplémentaires
|
|
179
|
+
|
|
179
180
|
Object.assign(jobQueue[id], data);
|
|
180
181
|
|
|
181
182
|
if (data.error) {
|
|
@@ -186,7 +187,7 @@ function updateJobStatus(id, status, data = {}) {
|
|
|
186
187
|
jobQueue[id].completedAt = new Date();
|
|
187
188
|
const duration = jobQueue[id].completedAt - jobQueue[id].createdAt;
|
|
188
189
|
jobQueue[id].duration = duration;
|
|
189
|
-
log('info', `Tâche ${id} terminée en ${(duration/1000).toFixed(2)}s`);
|
|
190
|
+
log('info', `Tâche ${id} terminée en ${(duration / 1000).toFixed(2)}s`);
|
|
190
191
|
} else {
|
|
191
192
|
log('info', `Tâche ${id}: ${oldStatus} -> ${status}`);
|
|
192
193
|
}
|
|
@@ -207,7 +208,7 @@ function getLogs(filter = {}) {
|
|
|
207
208
|
if (!filter || Object.keys(filter).length === 0) {
|
|
208
209
|
return logHistory;
|
|
209
210
|
}
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
return logHistory.filter(log => {
|
|
212
213
|
if (filter.level && log.level !== filter.level) return false;
|
|
213
214
|
if (filter.since && new Date(log.timestamp) < new Date(filter.since)) return false;
|
|
@@ -216,47 +217,57 @@ function getLogs(filter = {}) {
|
|
|
216
217
|
});
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
// Nettoyer les vieilles tâches terminées
|
|
220
220
|
function cleanOldJobs() {
|
|
221
221
|
const now = Date.now();
|
|
222
222
|
const toDelete = [];
|
|
223
|
-
|
|
223
|
+
const MAX_AGE = 86400000;
|
|
224
|
+
|
|
224
225
|
for (const [id, job] of Object.entries(jobQueue)) {
|
|
225
|
-
const
|
|
226
|
-
|
|
226
|
+
const createdAt = job.createdAt ? new Date(job.createdAt).getTime() : now;
|
|
227
|
+
if (isNaN(createdAt)) {
|
|
228
|
+
log('warn', `Tâche ${id} a une date de création invalide, conservation`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const age = now - createdAt;
|
|
233
|
+
|
|
234
|
+
if (age < 0) {
|
|
235
|
+
log('warn', `Tâche ${id} a une date dans le futur, conservation`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const isOld = age > MAX_AGE;
|
|
227
240
|
const isCompleted = ['completed', 'failed'].includes(job.status);
|
|
228
|
-
|
|
241
|
+
|
|
229
242
|
if (isOld && isCompleted) {
|
|
230
243
|
toDelete.push(id);
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
|
-
|
|
246
|
+
|
|
234
247
|
for (const id of toDelete) {
|
|
235
248
|
delete jobQueue[id];
|
|
236
249
|
}
|
|
237
|
-
|
|
250
|
+
|
|
238
251
|
if (toDelete.length > 0) {
|
|
239
252
|
log('info', `${toDelete.length} vieilles tâches supprimées de la queue`);
|
|
240
253
|
isDirty = true;
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
256
|
|
|
244
|
-
// Réessayer une tâche échouée ou crashed
|
|
245
257
|
async function retryJob(id) {
|
|
246
258
|
const job = jobQueue[id];
|
|
247
259
|
if (!job) {
|
|
248
260
|
throw new Error(`Tâche ${id} introuvable`);
|
|
249
261
|
}
|
|
250
|
-
|
|
262
|
+
|
|
251
263
|
if (!['failed', 'crashed'].includes(job.status)) {
|
|
252
264
|
throw new Error(`La tâche ${id} ne peut pas être réessayée (statut: ${job.status})`);
|
|
253
265
|
}
|
|
254
|
-
|
|
266
|
+
|
|
255
267
|
if (job.retryCount >= job.maxRetries) {
|
|
256
268
|
throw new Error(`La tâche ${id} a atteint le nombre max de tentatives (${job.maxRetries})`);
|
|
257
269
|
}
|
|
258
|
-
|
|
259
|
-
// Créer une nouvelle tâche basée sur l'ancienne
|
|
270
|
+
|
|
260
271
|
const newJob = {
|
|
261
272
|
...job,
|
|
262
273
|
id: uuidv4().split('-')[0],
|
|
@@ -268,29 +279,27 @@ async function retryJob(id) {
|
|
|
268
279
|
error: null,
|
|
269
280
|
output: null
|
|
270
281
|
};
|
|
271
|
-
|
|
282
|
+
|
|
272
283
|
delete newJob.failedAt;
|
|
273
284
|
delete newJob.crashedAt;
|
|
274
285
|
delete newJob.completedAt;
|
|
275
|
-
|
|
286
|
+
|
|
276
287
|
jobQueue[newJob.id] = newJob;
|
|
277
288
|
isDirty = true;
|
|
278
|
-
|
|
289
|
+
|
|
279
290
|
log('info', `Tâche ${id} réessayée -> nouvelle tâche ${newJob.id} (tentative ${newJob.retryCount}/${newJob.maxRetries})`);
|
|
280
|
-
|
|
291
|
+
|
|
281
292
|
return newJob;
|
|
282
293
|
}
|
|
283
294
|
|
|
284
|
-
// Obtenir les tâches crashées qui peuvent être reprises
|
|
285
295
|
function getCrashedJobs() {
|
|
286
|
-
return Object.values(jobQueue).filter(job =>
|
|
287
|
-
job.status === 'crashed' &&
|
|
296
|
+
return Object.values(jobQueue).filter(job =>
|
|
297
|
+
job.status === 'crashed' &&
|
|
288
298
|
job.canRetry &&
|
|
289
299
|
job.retryCount < job.maxRetries
|
|
290
300
|
);
|
|
291
301
|
}
|
|
292
302
|
|
|
293
|
-
// Statistiques de la queue
|
|
294
303
|
function getStats() {
|
|
295
304
|
const stats = {
|
|
296
305
|
total: Object.keys(jobQueue).length,
|
|
@@ -299,79 +308,56 @@ function getStats() {
|
|
|
299
308
|
avgDuration: 0,
|
|
300
309
|
successRate: 0
|
|
301
310
|
};
|
|
302
|
-
|
|
311
|
+
|
|
303
312
|
let totalDuration = 0;
|
|
304
313
|
let completedCount = 0;
|
|
305
|
-
|
|
314
|
+
|
|
306
315
|
for (const job of Object.values(jobQueue)) {
|
|
307
|
-
// Par statut
|
|
308
316
|
stats.byStatus[job.status] = (stats.byStatus[job.status] || 0) + 1;
|
|
309
|
-
|
|
310
|
-
// Par type
|
|
311
317
|
stats.byType[job.type] = (stats.byType[job.type] || 0) + 1;
|
|
312
|
-
|
|
313
|
-
// Durée moyenne
|
|
318
|
+
|
|
314
319
|
if (job.duration) {
|
|
315
320
|
totalDuration += job.duration;
|
|
316
321
|
completedCount++;
|
|
317
322
|
}
|
|
318
323
|
}
|
|
319
|
-
|
|
324
|
+
|
|
320
325
|
if (completedCount > 0) {
|
|
321
326
|
stats.avgDuration = Math.round(totalDuration / completedCount);
|
|
322
327
|
}
|
|
323
|
-
|
|
328
|
+
|
|
324
329
|
const totalFinished = (stats.byStatus.completed || 0) + (stats.byStatus.failed || 0);
|
|
325
330
|
if (totalFinished > 0) {
|
|
326
331
|
stats.successRate = Math.round((stats.byStatus.completed || 0) / totalFinished * 100);
|
|
327
332
|
}
|
|
328
|
-
|
|
333
|
+
|
|
329
334
|
return stats;
|
|
330
335
|
}
|
|
331
336
|
|
|
332
|
-
// Initialiser le module
|
|
333
337
|
async function init() {
|
|
334
338
|
await loadQueue();
|
|
335
339
|
startAutoSave();
|
|
336
|
-
|
|
337
|
-
// Nettoyer périodiquement
|
|
338
|
-
setInterval(cleanOldJobs, 3600000); // Toutes les heures
|
|
340
|
+
setInterval(cleanOldJobs, 3600000);
|
|
339
341
|
}
|
|
340
342
|
|
|
341
|
-
// Arrêter proprement
|
|
342
343
|
async function shutdown() {
|
|
343
344
|
log('info', 'Arrêt du gestionnaire de queue...');
|
|
344
345
|
stopAutoSave();
|
|
345
346
|
await saveQueue();
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
process.on('SIGTERM', async () => {
|
|
355
|
-
await shutdown();
|
|
356
|
-
process.exit(0);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Démarrer automatiquement
|
|
360
|
-
init().catch(err => {
|
|
361
|
-
console.error('Erreur lors de l\'initialisation de la queue:', err);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
export default {
|
|
365
|
-
addJob,
|
|
366
|
-
updateJobStatus,
|
|
367
|
-
getJob,
|
|
368
|
-
getQueue,
|
|
369
|
-
getLogs,
|
|
349
|
+
export default {
|
|
350
|
+
addJob,
|
|
351
|
+
updateJobStatus,
|
|
352
|
+
getJob,
|
|
353
|
+
getQueue,
|
|
354
|
+
getLogs,
|
|
370
355
|
log,
|
|
371
356
|
retryJob,
|
|
372
357
|
getCrashedJobs,
|
|
373
358
|
getStats,
|
|
374
359
|
cleanOldJobs,
|
|
375
360
|
saveQueue,
|
|
376
|
-
shutdown
|
|
361
|
+
shutdown,
|
|
362
|
+
init
|
|
377
363
|
};
|