@fkom13/mcp-sftp-orchestrator 11.3.0 → 11.8.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 +6 -0
- package/CHANGELOG.md +174 -0
- package/README.fr.md +230 -243
- package/README.md +268 -286
- package/apis.js +20 -81
- package/atomicJsonStore.js +117 -0
- package/config.js +5 -0
- package/fleet.js +170 -0
- package/groups.js +25 -0
- package/history.js +5 -51
- package/infraTopology.js +336 -0
- package/inventory.js +151 -0
- package/lhm.plugin.json +7 -0
- package/notes.js +3 -87
- package/package.json +8 -6
- package/policies.js +6 -74
- package/projects.js +108 -0
- package/queue.js +83 -14
- package/server.js +821 -90
- package/servers.js +30 -33
- package/sftp.js +168 -60
- package/shellSessions.js +31 -3
- package/snapshotManager.js +3 -7
- package/sourceAdapter.js +54 -1
- package/ssh.js +17 -3
- package/sshPool.js +129 -112
- package/sshTrust.js +214 -0
- package/toolMetadata.js +76 -0
- package/tunnels.js +36 -17
- package/utils.js +210 -1
- package/workSession.js +139 -0
- package/.gencodedoc.yaml +0 -76
- package/ROADMAP.md +0 -21
- package/test_features.js +0 -112
- package/test_mcp.js +0 -55
package/apis.js
CHANGED
|
@@ -1,105 +1,44 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import config from './config.js';
|
|
3
|
+
import jsonStore from './atomicJsonStore.js';
|
|
4
4
|
|
|
5
5
|
const APIS_FILE = path.join(config.dataDir, 'apis.json');
|
|
6
|
+
const EMPTY = {};
|
|
6
7
|
|
|
7
|
-
let apis = {};
|
|
8
|
-
|
|
9
|
-
let isInitialized = false;
|
|
10
|
-
let initPromise = null;
|
|
11
|
-
|
|
12
|
-
// Charger les APIs au démarrage
|
|
13
8
|
async function loadApis() {
|
|
14
|
-
|
|
15
|
-
const data = await fs.readFile(APIS_FILE, 'utf-8');
|
|
16
|
-
apis = JSON.parse(data);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
if (err.code === 'ENOENT') {
|
|
19
|
-
// Le fichier n'existe pas encore, c'est normal
|
|
20
|
-
apis = {};
|
|
21
|
-
} else {
|
|
22
|
-
console.error("Erreur lors du chargement de apis.json:", err);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function ensureInitialized() {
|
|
28
|
-
if (isInitialized) return;
|
|
29
|
-
if (!initPromise) {
|
|
30
|
-
initPromise = loadApis().then(() => { isInitialized = true; });
|
|
31
|
-
}
|
|
32
|
-
return initPromise;
|
|
9
|
+
return jsonStore.readJson(APIS_FILE, EMPTY);
|
|
33
10
|
}
|
|
34
11
|
|
|
35
|
-
// Sauvegarder les APIs
|
|
36
|
-
async function saveApis() {
|
|
37
|
-
await ensureInitialized();
|
|
38
|
-
try {
|
|
39
|
-
await fs.writeFile(APIS_FILE, JSON.stringify(apis, null, 2));
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.error("Erreur lors de la sauvegarde de apis.json:", err);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Ajouter ou mettre à jour une API
|
|
46
12
|
async function addApi(alias, apiConfig) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
throw new Error("L'URL de l'API est obligatoire.");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Valider l'URL
|
|
59
|
-
try {
|
|
60
|
-
new URL(apiConfig.url);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
throw new Error(`URL invalide: ${apiConfig.url}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
apis[alias] = apiConfig;
|
|
66
|
-
await saveApis();
|
|
13
|
+
if (!alias || typeof alias !== 'string') throw new Error("L'alias doit être une chaîne non vide.");
|
|
14
|
+
if (!apiConfig.url) throw new Error("L'URL de l'API est obligatoire.");
|
|
15
|
+
try { new URL(apiConfig.url); } catch { throw new Error(`URL invalide: ${apiConfig.url}`); }
|
|
16
|
+
|
|
17
|
+
await jsonStore.updateJson(APIS_FILE, EMPTY, (apis) => {
|
|
18
|
+
apis[alias] = apiConfig;
|
|
19
|
+
return apis;
|
|
20
|
+
});
|
|
67
21
|
return { success: true, message: `API '${alias}' ajoutée/mise à jour avec succès.` };
|
|
68
22
|
}
|
|
69
23
|
|
|
70
|
-
// Lister toutes les APIs
|
|
71
24
|
async function listApis() {
|
|
72
|
-
|
|
73
|
-
return apis;
|
|
25
|
+
return loadApis();
|
|
74
26
|
}
|
|
75
27
|
|
|
76
|
-
// Obtenir une API par son alias
|
|
77
28
|
async function getApi(alias) {
|
|
78
|
-
await
|
|
29
|
+
const apis = await loadApis();
|
|
79
30
|
const apiConfig = apis[alias];
|
|
80
|
-
if (!apiConfig) {
|
|
81
|
-
throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
82
|
-
}
|
|
31
|
+
if (!apiConfig) throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
83
32
|
return apiConfig;
|
|
84
33
|
}
|
|
85
34
|
|
|
86
|
-
// Supprimer une API
|
|
87
35
|
async function removeApi(alias) {
|
|
88
|
-
await
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await saveApis();
|
|
36
|
+
await jsonStore.updateJson(APIS_FILE, EMPTY, (apis) => {
|
|
37
|
+
if (!apis[alias]) throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
38
|
+
delete apis[alias];
|
|
39
|
+
return apis;
|
|
40
|
+
});
|
|
94
41
|
return { success: true, message: `API '${alias}' supprimée avec succès.` };
|
|
95
42
|
}
|
|
96
43
|
|
|
97
|
-
|
|
98
|
-
ensureInitialized();
|
|
99
|
-
|
|
100
|
-
export default {
|
|
101
|
-
addApi,
|
|
102
|
-
listApis,
|
|
103
|
-
getApi,
|
|
104
|
-
removeApi
|
|
105
|
-
};
|
|
44
|
+
export default { addApi, listApis, getApi, removeApi };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AtomicJsonStore — primitive commune pour les petits registres JSON du MCP.
|
|
7
|
+
*
|
|
8
|
+
* Garanties intra-processus :
|
|
9
|
+
* - update(file) sérialise read -> mutate -> write par fichier ;
|
|
10
|
+
* - écriture via temp unique + fsync + rename atomique ;
|
|
11
|
+
* - backup .bak du dernier fichier valide avant remplacement ;
|
|
12
|
+
* - permissions du temp forcées à 0600 (les registres peuvent contenir des secrets) ;
|
|
13
|
+
* - un JSON corrompu n'est plus silencieusement remplacé par {} : tentative .bak,
|
|
14
|
+
* puis erreur explicite si aucune copie valide n'existe.
|
|
15
|
+
*
|
|
16
|
+
* Ce verrou protège les appels concurrents du même process MCP. Pour le SaaS
|
|
17
|
+
* multi-instance, la persistance devra migrer vers DB/transaction ou un lock distribué.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const tails = new Map();
|
|
21
|
+
|
|
22
|
+
function clone(value) {
|
|
23
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function withLock(file, fn) {
|
|
27
|
+
const key = path.resolve(file);
|
|
28
|
+
const prev = tails.get(key) || Promise.resolve();
|
|
29
|
+
let release;
|
|
30
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
31
|
+
const tail = prev.catch(() => {}).then(() => gate);
|
|
32
|
+
tails.set(key, tail);
|
|
33
|
+
|
|
34
|
+
await prev.catch(() => {});
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
} finally {
|
|
38
|
+
release();
|
|
39
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function parseFile(file) {
|
|
44
|
+
const text = await fs.readFile(file, 'utf8');
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readJson(file, fallback, { createIfMissing = false } = {}) {
|
|
49
|
+
try {
|
|
50
|
+
return await parseFile(file);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err?.code === 'ENOENT') {
|
|
53
|
+
const value = clone(fallback);
|
|
54
|
+
if (createIfMissing) await writeJsonAtomic(file, value, { backup: false });
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse/I/O error : récupérer la dernière copie saine si possible.
|
|
59
|
+
try {
|
|
60
|
+
return await parseFile(`${file}.bak`);
|
|
61
|
+
} catch {
|
|
62
|
+
const wrapped = new Error(`JSON store illisible: ${file} (${err.message})`);
|
|
63
|
+
wrapped.code = 'JSON_STORE_CORRUPT';
|
|
64
|
+
wrapped.cause = err;
|
|
65
|
+
throw wrapped;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeJsonAtomic(file, value, { backup = true } = {}) {
|
|
71
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
72
|
+
|
|
73
|
+
if (backup) {
|
|
74
|
+
try { await fs.copyFile(file, `${file}.bak`); } catch (e) {
|
|
75
|
+
if (e?.code !== 'ENOENT') throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
80
|
+
let handle = null;
|
|
81
|
+
try {
|
|
82
|
+
handle = await fs.open(tmp, 'w', 0o600);
|
|
83
|
+
await handle.writeFile(JSON.stringify(value, null, 2));
|
|
84
|
+
await handle.sync();
|
|
85
|
+
await handle.close();
|
|
86
|
+
handle = null;
|
|
87
|
+
await fs.rename(tmp, file);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (handle) await handle.close().catch(() => {});
|
|
90
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureJson(file, fallback) {
|
|
96
|
+
return withLock(file, async () => readJson(file, fallback, { createIfMissing: true }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function updateJson(file, fallback, mutator, options = {}) {
|
|
100
|
+
return withLock(file, async () => {
|
|
101
|
+
const current = await readJson(file, fallback, { createIfMissing: false });
|
|
102
|
+
const next = await mutator(current);
|
|
103
|
+
const value = next === undefined ? current : next;
|
|
104
|
+
await writeJsonAtomic(file, value, options);
|
|
105
|
+
return value;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
withLock,
|
|
111
|
+
readJson,
|
|
112
|
+
writeJsonAtomic,
|
|
113
|
+
ensureJson,
|
|
114
|
+
updateJson
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { withLock, readJson, writeJsonAtomic, ensureJson, updateJson };
|
package/config.js
CHANGED
|
@@ -21,6 +21,7 @@ const args = process.argv.slice(2).reduce((acc, arg) => {
|
|
|
21
21
|
}, {});
|
|
22
22
|
|
|
23
23
|
const config = {
|
|
24
|
+
version: '11.8.0',
|
|
24
25
|
dataDir: args['data-dir'] || process.env.MCP_DATA_DIR || path.join(os.homedir(), '.config', 'mcp-orchestrator'),
|
|
25
26
|
|
|
26
27
|
// Timeouts en millisecondes (plus cohérent)
|
|
@@ -37,6 +38,10 @@ const config = {
|
|
|
37
38
|
|
|
38
39
|
// Sécurité
|
|
39
40
|
allowedRoots: (process.env.MCP_ALLOWED_ROOTS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
41
|
+
// Mode lecture seule : refuse écritures / exec mutantes (exploration safe)
|
|
42
|
+
readOnly: process.env.MCP_READONLY === 'true' || process.env.MCP_READONLY === '1',
|
|
43
|
+
// Réponses compactes (moins de tokens agent)
|
|
44
|
+
compact: process.env.MCP_COMPACT === 'true' || process.env.MCP_COMPACT === '1',
|
|
40
45
|
|
|
41
46
|
// Configuration Queue
|
|
42
47
|
maxQueueSize: parseInt(process.env.MAX_QUEUE_SIZE || '1000', 10),
|
package/fleet.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet.js — Diagnostic parallèle du parc (ping SSH + ressources légères).
|
|
3
|
+
*/
|
|
4
|
+
import queue from './queue.js';
|
|
5
|
+
import servers from './servers.js';
|
|
6
|
+
import sshPool from './sshPool.js';
|
|
7
|
+
import utils from './utils.js';
|
|
8
|
+
|
|
9
|
+
const PROBE_CMD = `echo OK; date -u +%Y-%m-%dT%H:%M:%SZ; uptime; free -h 2>/dev/null | head -2; df -h / 2>/dev/null | tail -1`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Probe un serveur : latence handshake + commande légère.
|
|
13
|
+
* Retourne { alias, ok, latencyMs, host, user, output?, error?, resources? }
|
|
14
|
+
*/
|
|
15
|
+
async function probeServer(alias, timeoutMs = 15000) {
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
let connection = null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const serverConfig = await servers.getServer(alias);
|
|
21
|
+
// Vérifier que la clé existe (fail rapide)
|
|
22
|
+
if (serverConfig.keyPath) {
|
|
23
|
+
const fs = await import('fs/promises');
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(serverConfig.keyPath);
|
|
26
|
+
} catch {
|
|
27
|
+
return {
|
|
28
|
+
alias,
|
|
29
|
+
ok: false,
|
|
30
|
+
latencyMs: Date.now() - started,
|
|
31
|
+
host: serverConfig.host,
|
|
32
|
+
user: serverConfig.user,
|
|
33
|
+
port: utils.resolveSshPort(serverConfig),
|
|
34
|
+
error: `Clé SSH introuvable: ${serverConfig.keyPath}`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connection = await Promise.race([
|
|
40
|
+
sshPool.getConnection(alias, serverConfig),
|
|
41
|
+
new Promise((_, reject) =>
|
|
42
|
+
setTimeout(() => reject(new Error(`Timeout connexion ${timeoutMs}ms`)), timeoutMs)
|
|
43
|
+
)
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const latencyMs = Date.now() - started;
|
|
47
|
+
const output = await execOnClient(connection.client, PROBE_CMD, Math.max(5000, timeoutMs - latencyMs));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
alias,
|
|
51
|
+
ok: true,
|
|
52
|
+
latencyMs,
|
|
53
|
+
host: serverConfig.host,
|
|
54
|
+
user: serverConfig.user,
|
|
55
|
+
port: utils.resolveSshPort(serverConfig),
|
|
56
|
+
output: (output || '').trim().slice(0, 800),
|
|
57
|
+
resources: parseLightResources(output)
|
|
58
|
+
};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
let host = null, user = null, port = 22;
|
|
61
|
+
try {
|
|
62
|
+
const sc = await servers.getServer(alias);
|
|
63
|
+
host = sc.host; user = sc.user; port = utils.resolveSshPort(sc);
|
|
64
|
+
} catch { /* alias inconnu */ }
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
alias,
|
|
68
|
+
ok: false,
|
|
69
|
+
latencyMs: Date.now() - started,
|
|
70
|
+
host,
|
|
71
|
+
user,
|
|
72
|
+
port,
|
|
73
|
+
error: err.message
|
|
74
|
+
};
|
|
75
|
+
} finally {
|
|
76
|
+
if (connection) {
|
|
77
|
+
try { sshPool.releaseConnection(connection.id); } catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function execOnClient(client, cmd, timeoutMs) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let settled = false;
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
if (!settled) {
|
|
87
|
+
settled = true;
|
|
88
|
+
reject(new Error(`Timeout exec ${timeoutMs}ms`));
|
|
89
|
+
}
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
|
|
92
|
+
client.exec(cmd, (err, stream) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (!settled) { settled = true; reject(err); }
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let out = '';
|
|
99
|
+
stream.on('data', d => { out += d.toString(); });
|
|
100
|
+
stream.stderr.on('data', d => { out += d.toString(); });
|
|
101
|
+
stream.on('close', (code) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (settled) return;
|
|
104
|
+
settled = true;
|
|
105
|
+
if (code !== 0 && code !== null) {
|
|
106
|
+
// Certaines distros n'ont pas free -h : on accepte quand même si OK en tête
|
|
107
|
+
if (out.includes('OK')) resolve(out);
|
|
108
|
+
else reject(new Error(`exit ${code}: ${out.slice(0, 200)}`));
|
|
109
|
+
} else {
|
|
110
|
+
resolve(out);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseLightResources(output) {
|
|
118
|
+
if (!output) return null;
|
|
119
|
+
const resources = { load: null, disk_use: null, mem_line: null };
|
|
120
|
+
try {
|
|
121
|
+
const loadMatch = output.match(/load average:\s*([0-9.,\s]+)/i);
|
|
122
|
+
if (loadMatch) resources.load = loadMatch[1].trim();
|
|
123
|
+
const lines = output.split('\n');
|
|
124
|
+
const mem = lines.find(l => /^\s*Mem:/i.test(l));
|
|
125
|
+
if (mem) resources.mem_line = mem.trim();
|
|
126
|
+
const disk = lines.find(l => /\/\s*$/.test(l.trim()) || l.includes('%'));
|
|
127
|
+
// dernière ligne df souvent "… 42% /"
|
|
128
|
+
const df = [...lines].reverse().find(l => /\d+%/.test(l) && l.includes('/'));
|
|
129
|
+
if (df) {
|
|
130
|
+
const m = df.match(/(\d+%)/);
|
|
131
|
+
if (m) resources.disk_use = m[1];
|
|
132
|
+
}
|
|
133
|
+
} catch { /* ignore */ }
|
|
134
|
+
return resources;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Status de tout le parc (ou d'une liste d'alias).
|
|
139
|
+
* options = { aliases?: string[], timeoutMs?: number }
|
|
140
|
+
*/
|
|
141
|
+
async function fleetStatus(options = {}) {
|
|
142
|
+
const all = await servers.listServers();
|
|
143
|
+
let aliases = options.aliases;
|
|
144
|
+
|
|
145
|
+
if (!aliases || aliases.length === 0) {
|
|
146
|
+
aliases = Object.keys(all);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Résolution group: / all déjà faite en amont idéalement
|
|
150
|
+
const timeoutMs = options.timeoutMs || 15000;
|
|
151
|
+
queue.log('info', `fleet_status: probe de ${aliases.length} serveur(s)...`);
|
|
152
|
+
|
|
153
|
+
const results = await Promise.all(
|
|
154
|
+
aliases.map(alias => probeServer(alias, timeoutMs))
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const ok = results.filter(r => r.ok).length;
|
|
158
|
+
const failed = results.filter(r => !r.ok);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
total: results.length,
|
|
162
|
+
online: ok,
|
|
163
|
+
offline: failed.length,
|
|
164
|
+
results: results.sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
165
|
+
failed_aliases: failed.map(r => r.alias),
|
|
166
|
+
timestamp: new Date().toISOString()
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default { probeServer, fleetStatus };
|
package/groups.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Groupes d'alias serveurs. Stockage atomique: <dataDir>/server_groups.json */
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import config from './config.js';
|
|
4
|
+
import servers from './servers.js';
|
|
5
|
+
import jsonStore from './atomicJsonStore.js';
|
|
6
|
+
const GROUPS_PATH = path.join(config.dataDir, 'server_groups.json');
|
|
7
|
+
const DEFAULT_GROUPS = { oci:['fkomprodmini1_prod','fkomprodmini2_prod','fkomprodmini2_vpn'], contabo:['vps_contabo'], vpsparfait:['vpsparfait_fkomp','vpsparfait2_fkomp'], local:['pc1_tailscale'] };
|
|
8
|
+
const loadGroups = () => jsonStore.readJson(GROUPS_PATH, DEFAULT_GROUPS);
|
|
9
|
+
async function listGroups(){ return loadGroups(); }
|
|
10
|
+
async function setGroup(name, aliases){
|
|
11
|
+
if(!name || !/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error("Nom de groupe invalide (alphanumérique, _ ou -).");
|
|
12
|
+
if(!Array.isArray(aliases)||!aliases.length) throw new Error("aliases doit être un tableau non vide d'alias serveurs.");
|
|
13
|
+
const all=await servers.listServers(); const unknown=aliases.filter(a=>!all[a]); if(unknown.length) throw new Error(`Alias inconnus: ${unknown.join(', ')}. Utilisez server_list.`);
|
|
14
|
+
const unique=[...new Set(aliases)]; await jsonStore.updateJson(GROUPS_PATH,DEFAULT_GROUPS,g=>{g[name]=unique;return g;}); return {success:true,group:name,aliases:unique};
|
|
15
|
+
}
|
|
16
|
+
async function removeGroup(name){ await jsonStore.updateJson(GROUPS_PATH,DEFAULT_GROUPS,g=>{if(!g[name]) throw new Error(`Groupe '${name}' introuvable.`); delete g[name]; return g;}); return {success:true,message:`Groupe '${name}' supprimé.`}; }
|
|
17
|
+
async function resolveAliases(aliasInput){
|
|
18
|
+
const allServers=await servers.listServers(), allKeys=Object.keys(allServers); if(aliasInput==null||aliasInput==='') throw new Error("Alias requis.");
|
|
19
|
+
if(Array.isArray(aliasInput)){ const expanded=[]; for(const a of aliasInput){ if(String(a).startsWith('group:')) expanded.push(...await expandGroup(String(a).slice(6))); else {if(!allServers[a]) throw new Error(`Alias inconnu: ${a}`); expanded.push(a);} } return [...new Set(expanded)]; }
|
|
20
|
+
if(aliasInput==='all') return allKeys; if(String(aliasInput).startsWith('group:')) return expandGroup(String(aliasInput).slice(6));
|
|
21
|
+
if(!allServers[aliasInput]){ const groups=await loadGroups(); if(groups[aliasInput]) return expandGroup(aliasInput); throw new Error(`Alias de serveur '${aliasInput}' est inconnu. Utilisez server_list ou group:<nom>.`); }
|
|
22
|
+
return [aliasInput];
|
|
23
|
+
}
|
|
24
|
+
async function expandGroup(name){ const groups=await loadGroups(); if(!groups[name]) throw new Error(`Groupe '${name}' introuvable. Groupes: ${Object.keys(groups).join(', ') || '(aucun)'}`); const all=await servers.listServers(); const aliases=groups[name].filter(a=>all[a]); if(!aliases.length) throw new Error(`Groupe '${name}' ne contient aucun alias valide actuellement.`); return aliases; }
|
|
25
|
+
export default {listGroups,setGroup,removeGroup,resolveAliases,expandGroup};
|
package/history.js
CHANGED
|
@@ -1,51 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async function readHistory() {
|
|
7
|
-
try {
|
|
8
|
-
await fs.access(HISTORY_FILE_PATH);
|
|
9
|
-
const data = await fs.readFile(HISTORY_FILE_PATH, 'utf-8');
|
|
10
|
-
return JSON.parse(data);
|
|
11
|
-
} catch (error) {
|
|
12
|
-
return [];
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function writeHistory(history) {
|
|
17
|
-
try {
|
|
18
|
-
await fs.writeFile(HISTORY_FILE_PATH, JSON.stringify(history, null, 2));
|
|
19
|
-
} catch (error) {
|
|
20
|
-
// Silently fail or log if possible, but don't crash
|
|
21
|
-
if (process.env.MCP_DEBUG === 'true') {
|
|
22
|
-
console.error(`[ERROR] Failed to write history: ${error.message}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function logTask(jobDetails) {
|
|
28
|
-
const history = await readHistory();
|
|
29
|
-
const logEntry = {
|
|
30
|
-
jobId: jobDetails.id,
|
|
31
|
-
timestamp: new Date().toISOString(),
|
|
32
|
-
type: jobDetails.type,
|
|
33
|
-
alias: jobDetails.alias,
|
|
34
|
-
command: jobDetails.type === 'ssh' ? jobDetails.cmd : `${jobDetails.direction} ${jobDetails.local} -> ${jobDetails.remote}`
|
|
35
|
-
};
|
|
36
|
-
history.unshift(logEntry); // Ajoute au début
|
|
37
|
-
if (history.length > 500) { // Garde les 500 dernières commandes
|
|
38
|
-
history.pop();
|
|
39
|
-
}
|
|
40
|
-
await writeHistory(history);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function getHistory(filters = {}) {
|
|
44
|
-
let history = await readHistory();
|
|
45
|
-
if (filters.alias) {
|
|
46
|
-
history = history.filter(log => log.alias === filters.alias);
|
|
47
|
-
}
|
|
48
|
-
return history;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default { logTask, getHistory };
|
|
1
|
+
import path from 'path'; import config from './config.js'; import jsonStore from './atomicJsonStore.js';
|
|
2
|
+
const HISTORY_FILE_PATH=path.join(config.dataDir,'history.json');
|
|
3
|
+
async function logTask(jobDetails){const logEntry={jobId:jobDetails.id,timestamp:new Date().toISOString(),type:jobDetails.type,alias:jobDetails.alias,command:jobDetails.type==='ssh'?jobDetails.cmd:`${jobDetails.direction} ${jobDetails.local} -> ${jobDetails.remote}`}; try{await jsonStore.updateJson(HISTORY_FILE_PATH,[],h=>{h.unshift(logEntry);if(h.length>500)h.length=500;return h;});}catch(error){if(process.env.MCP_DEBUG==='true')console.error(`[ERROR] Failed to write history: ${error.message}`);}}
|
|
4
|
+
async function getHistory(filters={}){let h=await jsonStore.readJson(HISTORY_FILE_PATH,[]);if(filters.alias)h=h.filter(x=>x.alias===filters.alias);return h;}
|
|
5
|
+
export default {logTask,getHistory};
|