@mgsoftwarebv/mg-dashboard-mcp 2.0.4 → 2.1.1
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/dist/index.js +177 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -311,10 +311,13 @@ async function syncEnvVarsToVercel(token, projectId, envVars) {
|
|
|
311
311
|
const data = await res.json().catch(() => ({}));
|
|
312
312
|
return { created: data?.created?.length ?? envVars.length, error: null };
|
|
313
313
|
}
|
|
314
|
-
async function attemptVercelSync(appName, environment) {
|
|
314
|
+
async function attemptVercelSync(appName, environment, knownStageId) {
|
|
315
315
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
let stageId = knownStageId;
|
|
317
|
+
if (!stageId) {
|
|
318
|
+
const { data: direct } = await supabase.from("env_config").select("release_profile_stage_id").eq("app_name", appName).not("release_profile_stage_id", "is", null).limit(1).single();
|
|
319
|
+
stageId = direct?.release_profile_stage_id;
|
|
320
|
+
}
|
|
318
321
|
if (!stageId) return "Vercel sync skipped: no stage link found";
|
|
319
322
|
const { data: settings } = await supabase.from("app_setting").select("vercel_token_encrypted").maybeSingle();
|
|
320
323
|
if (!settings?.vercel_token_encrypted) return "Vercel sync skipped: no Vercel token configured";
|
|
@@ -383,12 +386,29 @@ async function attemptVercelSync(appName, environment) {
|
|
|
383
386
|
return `Vercel sync error: ${msg}`;
|
|
384
387
|
}
|
|
385
388
|
}
|
|
389
|
+
var SSH_PROXY_SERVER_ID = "03659d55-e194-400d-b82a-bf6457371ded";
|
|
390
|
+
var _proxyConnCache = null;
|
|
391
|
+
async function getProxyConnection() {
|
|
392
|
+
if (_proxyConnCache) return _proxyConnCache;
|
|
393
|
+
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted").eq("id", SSH_PROXY_SERVER_ID).single();
|
|
394
|
+
if (error || !data) throw new Error("SSH Proxy server not found in database");
|
|
395
|
+
if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
|
|
396
|
+
_proxyConnCache = {
|
|
397
|
+
hostname: data.hostname,
|
|
398
|
+
port: data.port || 22,
|
|
399
|
+
username: data.username,
|
|
400
|
+
password: data.password_encrypted ? decrypt(data.password_encrypted) : void 0,
|
|
401
|
+
privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
|
|
402
|
+
passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
|
|
403
|
+
};
|
|
404
|
+
return _proxyConnCache;
|
|
405
|
+
}
|
|
386
406
|
async function getServerConnection(serverId) {
|
|
387
407
|
assertServerAccess(serverId);
|
|
388
|
-
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted").eq("id", serverId).single();
|
|
408
|
+
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted, allowed_ssh_ips").eq("id", serverId).single();
|
|
389
409
|
if (error || !data) throw new Error(`Server not found: ${serverId}`);
|
|
390
410
|
if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
|
|
391
|
-
|
|
411
|
+
const conn = {
|
|
392
412
|
hostname: data.hostname,
|
|
393
413
|
port: data.port || 22,
|
|
394
414
|
username: data.username,
|
|
@@ -396,8 +416,12 @@ async function getServerConnection(serverId) {
|
|
|
396
416
|
privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
|
|
397
417
|
passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
|
|
398
418
|
};
|
|
419
|
+
const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
|
|
420
|
+
const proxy = needsProxy ? await getProxyConnection() : void 0;
|
|
421
|
+
return { conn, proxy };
|
|
399
422
|
}
|
|
400
|
-
async function sshExec(opts, command) {
|
|
423
|
+
async function sshExec(opts, command, proxy) {
|
|
424
|
+
if (proxy) return sshExecViaProxy(proxy, opts, command);
|
|
401
425
|
return new Promise((resolve) => {
|
|
402
426
|
const ssh = new Client();
|
|
403
427
|
let stdout = "";
|
|
@@ -456,6 +480,98 @@ async function sshExec(opts, command) {
|
|
|
456
480
|
});
|
|
457
481
|
});
|
|
458
482
|
}
|
|
483
|
+
function sshExecViaProxy(proxyOpts, targetOpts, command) {
|
|
484
|
+
return new Promise((resolve) => {
|
|
485
|
+
const proxyClient = new Client();
|
|
486
|
+
let done = false;
|
|
487
|
+
const timeout = targetOpts.timeout || 6e4;
|
|
488
|
+
const timer = setTimeout(() => {
|
|
489
|
+
if (!done) {
|
|
490
|
+
done = true;
|
|
491
|
+
proxyClient.end();
|
|
492
|
+
resolve({ stdout: "", stderr: "SSH proxy command timeout", exitCode: -1 });
|
|
493
|
+
}
|
|
494
|
+
}, timeout);
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
clearTimeout(timer);
|
|
497
|
+
proxyClient.end();
|
|
498
|
+
};
|
|
499
|
+
proxyClient.on("ready", () => {
|
|
500
|
+
proxyClient.forwardOut("127.0.0.1", 0, targetOpts.hostname, targetOpts.port, (err, tunnel) => {
|
|
501
|
+
if (err) {
|
|
502
|
+
if (!done) {
|
|
503
|
+
done = true;
|
|
504
|
+
cleanup();
|
|
505
|
+
resolve({ stdout: "", stderr: err.message, exitCode: -1 });
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const targetClient = new Client();
|
|
510
|
+
let stdout = "";
|
|
511
|
+
let stderr = "";
|
|
512
|
+
targetClient.on("ready", () => {
|
|
513
|
+
targetClient.exec(command, (execErr, stream) => {
|
|
514
|
+
if (execErr) {
|
|
515
|
+
if (!done) {
|
|
516
|
+
done = true;
|
|
517
|
+
targetClient.end();
|
|
518
|
+
cleanup();
|
|
519
|
+
resolve({ stdout, stderr, exitCode: -1 });
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
stream.on("data", (d) => {
|
|
524
|
+
stdout += d.toString();
|
|
525
|
+
});
|
|
526
|
+
stream.stderr.on("data", (d) => {
|
|
527
|
+
stderr += d.toString();
|
|
528
|
+
});
|
|
529
|
+
stream.on("close", (code) => {
|
|
530
|
+
if (!done) {
|
|
531
|
+
done = true;
|
|
532
|
+
targetClient.end();
|
|
533
|
+
cleanup();
|
|
534
|
+
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
targetClient.on("error", (targetErr) => {
|
|
540
|
+
if (!done) {
|
|
541
|
+
done = true;
|
|
542
|
+
targetClient.end();
|
|
543
|
+
cleanup();
|
|
544
|
+
resolve({ stdout, stderr: targetErr.message, exitCode: -1 });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
targetClient.connect({
|
|
548
|
+
sock: tunnel,
|
|
549
|
+
username: targetOpts.username,
|
|
550
|
+
password: targetOpts.password,
|
|
551
|
+
privateKey: targetOpts.privateKey,
|
|
552
|
+
passphrase: targetOpts.passphrase,
|
|
553
|
+
readyTimeout: timeout
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
proxyClient.on("error", (err) => {
|
|
558
|
+
if (!done) {
|
|
559
|
+
done = true;
|
|
560
|
+
cleanup();
|
|
561
|
+
resolve({ stdout: "", stderr: err.message, exitCode: -1 });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
proxyClient.connect({
|
|
565
|
+
host: proxyOpts.hostname,
|
|
566
|
+
port: proxyOpts.port,
|
|
567
|
+
username: proxyOpts.username,
|
|
568
|
+
password: proxyOpts.password,
|
|
569
|
+
privateKey: proxyOpts.privateKey,
|
|
570
|
+
passphrase: proxyOpts.passphrase,
|
|
571
|
+
readyTimeout: proxyOpts.timeout || 3e4
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
459
575
|
function sanitizePath(path) {
|
|
460
576
|
let normalized = path.replace(/\\/g, "/").replace(/\0/g, "");
|
|
461
577
|
const parts = normalized.split("/");
|
|
@@ -740,7 +856,7 @@ function assertAllowedLogPath(filePath) {
|
|
|
740
856
|
throw new Error(`Path not allowed. Must be under: ${ALLOWED_LOG_PREFIXES.join(", ")}`);
|
|
741
857
|
}
|
|
742
858
|
}
|
|
743
|
-
async function discoverSiteDatabases(conn) {
|
|
859
|
+
async function discoverSiteDatabases(conn, proxy) {
|
|
744
860
|
const script = `
|
|
745
861
|
check_dir() {
|
|
746
862
|
local base="$1" root="$2"
|
|
@@ -777,7 +893,7 @@ for dir in /var/www/*/; do
|
|
|
777
893
|
done
|
|
778
894
|
done
|
|
779
895
|
`.trim();
|
|
780
|
-
const result = await sshExec(conn, script);
|
|
896
|
+
const result = await sshExec(conn, script, proxy);
|
|
781
897
|
const sites = [];
|
|
782
898
|
for (const line of result.stdout.split("\n")) {
|
|
783
899
|
if (!line.trim()) continue;
|
|
@@ -861,9 +977,9 @@ DB_PORT=\${DB_PORT:-3306}
|
|
|
861
977
|
mysql --user="$DB_USER" --password="$DB_PASS" --host="$DB_HOST" --port="$DB_PORT" -t -e '${safeQuery}' "$DB_NAME" 2>&1 | grep -v "\\[Warning\\].*password"
|
|
862
978
|
`.trim();
|
|
863
979
|
}
|
|
864
|
-
async function execSiteMysql(conn, sitePath, query) {
|
|
980
|
+
async function execSiteMysql(conn, sitePath, query, proxy) {
|
|
865
981
|
const cmd = buildSiteMysqlCommand(sitePath, query);
|
|
866
|
-
const result = await sshExec(conn, cmd);
|
|
982
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
867
983
|
const output = (result.stdout || "").trim();
|
|
868
984
|
if (output.startsWith("ERROR: No database config found")) {
|
|
869
985
|
throw new Error(output);
|
|
@@ -1335,9 +1451,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1335
1451
|
return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
|
|
1336
1452
|
}
|
|
1337
1453
|
case "server-status": {
|
|
1338
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1454
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1339
1455
|
const cmd = 'echo "=== UPTIME ===" && uptime && echo "=== DISK ===" && df -h --total && echo "=== MEMORY ===" && free -h && echo "=== LOAD ===" && cat /proc/loadavg';
|
|
1340
|
-
const result = await sshExec(conn, cmd);
|
|
1456
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
1341
1457
|
const output = result.exitCode === 0 ? result.stdout : `Exit ${result.exitCode}
|
|
1342
1458
|
${result.stdout}
|
|
1343
1459
|
${result.stderr}`;
|
|
@@ -1347,9 +1463,9 @@ ${result.stderr}`;
|
|
|
1347
1463
|
case "ssh-execute": {
|
|
1348
1464
|
const command = String(a.command);
|
|
1349
1465
|
assertSafeCommand(command);
|
|
1350
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1466
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1351
1467
|
if (a.timeout) conn.timeout = Number(a.timeout);
|
|
1352
|
-
const result = await sshExec(conn, command);
|
|
1468
|
+
const result = await sshExec(conn, command, proxy);
|
|
1353
1469
|
const output = [`Exit code: ${result.exitCode}`];
|
|
1354
1470
|
if (result.stdout) output.push(`--- stdout ---
|
|
1355
1471
|
${result.stdout}`);
|
|
@@ -1358,45 +1474,45 @@ ${result.stderr}`);
|
|
|
1358
1474
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
1359
1475
|
}
|
|
1360
1476
|
case "server-reboot": {
|
|
1361
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1362
|
-
const result = await sshExec(conn, "sudo reboot");
|
|
1477
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1478
|
+
const result = await sshExec(conn, "sudo reboot", proxy);
|
|
1363
1479
|
return { content: [{ type: "text", text: result.exitCode === 0 ? "Reboot command sent. Server will be unavailable shortly." : `Reboot failed: ${result.stderr}` }] };
|
|
1364
1480
|
}
|
|
1365
1481
|
case "server-restart-service": {
|
|
1366
1482
|
const service = String(a.serviceName).replace(/[^a-zA-Z0-9._@-]/g, "");
|
|
1367
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1368
|
-
const result = await sshExec(conn, `sudo systemctl restart ${service}
|
|
1483
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1484
|
+
const result = await sshExec(conn, `sudo systemctl restart ${service}`, proxy);
|
|
1369
1485
|
if (result.exitCode === 0) {
|
|
1370
|
-
const status = await sshExec(conn, `sudo systemctl is-active ${service}
|
|
1486
|
+
const status = await sshExec(conn, `sudo systemctl is-active ${service}`, proxy);
|
|
1371
1487
|
return { content: [{ type: "text", text: `Service "${service}" restarted. Status: ${status.stdout.trim()}` }] };
|
|
1372
1488
|
}
|
|
1373
1489
|
return { content: [{ type: "text", text: `Failed to restart "${service}": ${result.stderr}` }] };
|
|
1374
1490
|
}
|
|
1375
|
-
// ----- SFTP -----
|
|
1491
|
+
// ----- SFTP (no proxy support yet — direct connection only) -----
|
|
1376
1492
|
case "sftp-list": {
|
|
1377
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1493
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1378
1494
|
const listing = await sftpReaddir(conn, String(a.path || "/"));
|
|
1379
1495
|
return { content: [{ type: "text", text: listing }] };
|
|
1380
1496
|
}
|
|
1381
1497
|
case "sftp-read": {
|
|
1382
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1498
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1383
1499
|
const content = await sftpRead(conn, String(a.path));
|
|
1384
1500
|
return { content: [{ type: "text", text: content }] };
|
|
1385
1501
|
}
|
|
1386
1502
|
case "sftp-write": {
|
|
1387
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1503
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1388
1504
|
const result = await sftpWrite(conn, String(a.path), String(a.content));
|
|
1389
1505
|
return { content: [{ type: "text", text: result }] };
|
|
1390
1506
|
}
|
|
1391
1507
|
case "sftp-delete": {
|
|
1392
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1508
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1393
1509
|
const result = await sftpDelete(conn, String(a.path));
|
|
1394
1510
|
return { content: [{ type: "text", text: result }] };
|
|
1395
1511
|
}
|
|
1396
1512
|
// ----- Docker -----
|
|
1397
1513
|
case "docker-list": {
|
|
1398
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1399
|
-
const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"');
|
|
1514
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1515
|
+
const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"', proxy);
|
|
1400
1516
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
1401
1517
|
}
|
|
1402
1518
|
case "docker-action": {
|
|
@@ -1405,22 +1521,22 @@ ${result.stderr}`);
|
|
|
1405
1521
|
if (!["start", "stop", "restart", "remove"].includes(action)) {
|
|
1406
1522
|
throw new Error(`Invalid action: ${action}. Use start, stop, restart, or remove.`);
|
|
1407
1523
|
}
|
|
1408
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1524
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1409
1525
|
const dockerCmd = action === "remove" ? `docker rm -f ${container}` : `docker ${action} ${container}`;
|
|
1410
|
-
const result = await sshExec(conn, dockerCmd);
|
|
1526
|
+
const result = await sshExec(conn, dockerCmd, proxy);
|
|
1411
1527
|
return { content: [{ type: "text", text: result.exitCode === 0 ? `Container "${container}" ${action}ed successfully` : `Error: ${result.stderr}` }] };
|
|
1412
1528
|
}
|
|
1413
1529
|
case "docker-logs": {
|
|
1414
1530
|
const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1415
1531
|
const lines = Number(a.lines) || 100;
|
|
1416
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1417
|
-
const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1
|
|
1532
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1533
|
+
const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1`, proxy);
|
|
1418
1534
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
1419
1535
|
}
|
|
1420
1536
|
// ----- Database -----
|
|
1421
1537
|
case "db-discover": {
|
|
1422
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1423
|
-
const sites = await discoverSiteDatabases(conn);
|
|
1538
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1539
|
+
const sites = await discoverSiteDatabases(conn, proxy);
|
|
1424
1540
|
if (!sites.length) {
|
|
1425
1541
|
return { content: [{ type: "text", text: "No web applications with database configs found in /var/www/" }] };
|
|
1426
1542
|
}
|
|
@@ -1430,26 +1546,27 @@ ${result.stderr}`);
|
|
|
1430
1546
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1431
1547
|
}
|
|
1432
1548
|
case "db-tables": {
|
|
1433
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1549
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1434
1550
|
const sql = "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024, 2) AS `Size (MB)`, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME";
|
|
1435
|
-
const output = await execSiteMysql(conn, String(a.sitePath), sql);
|
|
1551
|
+
const output = await execSiteMysql(conn, String(a.sitePath), sql, proxy);
|
|
1436
1552
|
return { content: [{ type: "text", text: output || "No tables found" }] };
|
|
1437
1553
|
}
|
|
1438
1554
|
case "db-describe": {
|
|
1439
1555
|
const table = String(a.table).replace(/[^a-zA-Z0-9_]/g, "");
|
|
1440
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1556
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1441
1557
|
const output = await execSiteMysql(
|
|
1442
1558
|
conn,
|
|
1443
1559
|
String(a.sitePath),
|
|
1444
|
-
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}
|
|
1560
|
+
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``,
|
|
1561
|
+
proxy
|
|
1445
1562
|
);
|
|
1446
1563
|
return { content: [{ type: "text", text: output }] };
|
|
1447
1564
|
}
|
|
1448
1565
|
case "db-query": {
|
|
1449
1566
|
const query = String(a.query).trim();
|
|
1450
1567
|
assertSafeSql(query);
|
|
1451
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1452
|
-
const output = await execSiteMysql(conn, String(a.sitePath), query);
|
|
1568
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1569
|
+
const output = await execSiteMysql(conn, String(a.sitePath), query, proxy);
|
|
1453
1570
|
return { content: [{ type: "text", text: output || "Query executed successfully (no output)" }] };
|
|
1454
1571
|
}
|
|
1455
1572
|
// ----- Env Config -----
|
|
@@ -1541,12 +1658,13 @@ ${result.stderr}`);
|
|
|
1541
1658
|
if (error) throw new Error(error.message);
|
|
1542
1659
|
saveMsg = `Stored env config: ${appName}/${environment}`;
|
|
1543
1660
|
}
|
|
1544
|
-
const
|
|
1661
|
+
const syncStageId = existing?.release_profile_stage_id ?? resolvedStageIds?.[0];
|
|
1662
|
+
const vercelStatus = await attemptVercelSync(appName, environment, syncStageId);
|
|
1545
1663
|
return { content: [{ type: "text", text: `${saveMsg}. ${vercelStatus}` }] };
|
|
1546
1664
|
}
|
|
1547
1665
|
// ----- Cache Purge -----
|
|
1548
1666
|
case "cache-purge": {
|
|
1549
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1667
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1550
1668
|
conn.timeout = 12e4;
|
|
1551
1669
|
const script = `
|
|
1552
1670
|
R=""
|
|
@@ -1627,13 +1745,13 @@ else
|
|
|
1627
1745
|
fi
|
|
1628
1746
|
echo -e "$R"
|
|
1629
1747
|
`.trim();
|
|
1630
|
-
const result = await sshExec(conn, script);
|
|
1748
|
+
const result = await sshExec(conn, script, proxy);
|
|
1631
1749
|
const output = (result.stdout || "").trim();
|
|
1632
1750
|
return { content: [{ type: "text", text: output || "Cache purge completed (no output)" }] };
|
|
1633
1751
|
}
|
|
1634
1752
|
// ----- Log Reading -----
|
|
1635
1753
|
case "log-list": {
|
|
1636
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1754
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1637
1755
|
const script = `
|
|
1638
1756
|
{
|
|
1639
1757
|
[ -d /usr/local/lsws/logs ] && find /usr/local/lsws/logs -maxdepth 2 -name "*.log" -type f 2>/dev/null
|
|
@@ -1659,7 +1777,7 @@ echo -e "$R"
|
|
|
1659
1777
|
echo "$f $HR $MOD"
|
|
1660
1778
|
done
|
|
1661
1779
|
`.trim();
|
|
1662
|
-
const result = await sshExec(conn, script);
|
|
1780
|
+
const result = await sshExec(conn, script, proxy);
|
|
1663
1781
|
return { content: [{ type: "text", text: result.stdout || "No log files found" }] };
|
|
1664
1782
|
}
|
|
1665
1783
|
case "log-read": {
|
|
@@ -1667,9 +1785,9 @@ done
|
|
|
1667
1785
|
assertAllowedLogPath(logPath);
|
|
1668
1786
|
const lineCount = Math.min(Math.max(Number(a.lines) || 100, 1), 500);
|
|
1669
1787
|
const filter = a.filter ? String(a.filter).replace(/'/g, "'\\''") : "";
|
|
1670
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1788
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1671
1789
|
const cmd = filter ? `grep '${filter}' '${logPath}' 2>/dev/null | tail -n ${lineCount}` : `tail -n ${lineCount} '${logPath}' 2>/dev/null`;
|
|
1672
|
-
const result = await sshExec(conn, cmd);
|
|
1790
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
1673
1791
|
if (result.exitCode !== 0 && !result.stdout) {
|
|
1674
1792
|
throw new Error(result.stderr || `Failed to read log: ${logPath}`);
|
|
1675
1793
|
}
|
|
@@ -1677,21 +1795,21 @@ done
|
|
|
1677
1795
|
}
|
|
1678
1796
|
// ----- Cron Jobs -----
|
|
1679
1797
|
case "cron-list": {
|
|
1680
|
-
const conn = await getServerConnection(a.serverId);
|
|
1798
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1681
1799
|
const user = a.user ? String(a.user) : null;
|
|
1682
1800
|
const parts = [];
|
|
1683
1801
|
if (user) {
|
|
1684
|
-
const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'
|
|
1802
|
+
const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`, proxy);
|
|
1685
1803
|
parts.push(`## Crontab for ${user}
|
|
1686
1804
|
${result.stdout}`);
|
|
1687
1805
|
} else {
|
|
1688
|
-
const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'
|
|
1806
|
+
const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`, proxy);
|
|
1689
1807
|
parts.push(`## Root crontab
|
|
1690
1808
|
${rootCron.stdout}`);
|
|
1691
|
-
const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'
|
|
1809
|
+
const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`, proxy);
|
|
1692
1810
|
parts.push(`## www-data crontab
|
|
1693
1811
|
${wwwCron.stdout}`);
|
|
1694
|
-
const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'
|
|
1812
|
+
const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'`, proxy);
|
|
1695
1813
|
parts.push(`## /etc/cron.d/
|
|
1696
1814
|
${cronD.stdout}`);
|
|
1697
1815
|
const summary = await sshExec(conn, [
|
|
@@ -1700,13 +1818,13 @@ ${cronD.stdout}`);
|
|
|
1700
1818
|
`echo "cron.daily: $(ls /etc/cron.daily/ 2>/dev/null | wc -l) scripts"`,
|
|
1701
1819
|
`echo "cron.weekly: $(ls /etc/cron.weekly/ 2>/dev/null | wc -l) scripts"`,
|
|
1702
1820
|
`echo "cron.monthly: $(ls /etc/cron.monthly/ 2>/dev/null | wc -l) scripts"`
|
|
1703
|
-
].join(" && "));
|
|
1821
|
+
].join(" && "), proxy);
|
|
1704
1822
|
parts.push(summary.stdout);
|
|
1705
1823
|
}
|
|
1706
1824
|
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
1707
1825
|
}
|
|
1708
1826
|
case "cron-add": {
|
|
1709
|
-
const conn = await getServerConnection(a.serverId);
|
|
1827
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1710
1828
|
const user = a.user ? String(a.user) : "root";
|
|
1711
1829
|
const schedule = String(a.schedule).trim();
|
|
1712
1830
|
const command = String(a.command).trim();
|
|
@@ -1716,7 +1834,7 @@ ${cronD.stdout}`);
|
|
|
1716
1834
|
}
|
|
1717
1835
|
const entry = commentText ? `# ${commentText}
|
|
1718
1836
|
${schedule} ${command}` : `${schedule} ${command}`;
|
|
1719
|
-
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user}
|
|
1837
|
+
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
|
|
1720
1838
|
if (result.exitCode !== 0) {
|
|
1721
1839
|
throw new Error(result.stderr || "Failed to add cron job");
|
|
1722
1840
|
}
|
|
@@ -1724,13 +1842,13 @@ ${schedule} ${command}` : `${schedule} ${command}`;
|
|
|
1724
1842
|
${schedule} ${command}` }] };
|
|
1725
1843
|
}
|
|
1726
1844
|
case "cron-remove": {
|
|
1727
|
-
const conn = await getServerConnection(a.serverId);
|
|
1845
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1728
1846
|
const user = a.user ? String(a.user) : "root";
|
|
1729
1847
|
const match = String(a.commandMatch).trim();
|
|
1730
1848
|
if (!match) {
|
|
1731
1849
|
throw new Error("commandMatch is required");
|
|
1732
1850
|
}
|
|
1733
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null
|
|
1851
|
+
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1734
1852
|
const lines = current.stdout.split("\n");
|
|
1735
1853
|
const before = lines.length;
|
|
1736
1854
|
const filtered = lines.filter((line) => {
|
|
@@ -1742,21 +1860,21 @@ ${schedule} ${command}` }] };
|
|
|
1742
1860
|
return { content: [{ type: "text", text: `No cron entries found matching "${match}"` }] };
|
|
1743
1861
|
}
|
|
1744
1862
|
const escapedCrontab = filtered.join("\n").replace(/'/g, "'\\''");
|
|
1745
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user}
|
|
1863
|
+
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
1746
1864
|
if (result.exitCode !== 0) {
|
|
1747
1865
|
throw new Error(result.stderr || "Failed to update crontab");
|
|
1748
1866
|
}
|
|
1749
1867
|
return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
|
|
1750
1868
|
}
|
|
1751
1869
|
case "cron-toggle": {
|
|
1752
|
-
const conn = await getServerConnection(a.serverId);
|
|
1870
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1753
1871
|
const user = a.user ? String(a.user) : "root";
|
|
1754
1872
|
const match = String(a.commandMatch).trim();
|
|
1755
1873
|
const enable = Boolean(a.enable);
|
|
1756
1874
|
if (!match) {
|
|
1757
1875
|
throw new Error("commandMatch is required");
|
|
1758
1876
|
}
|
|
1759
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null
|
|
1877
|
+
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1760
1878
|
const lines = current.stdout.split("\n");
|
|
1761
1879
|
let toggled = 0;
|
|
1762
1880
|
const updated = lines.map((line) => {
|
|
@@ -1775,7 +1893,7 @@ ${schedule} ${command}` }] };
|
|
|
1775
1893
|
return { content: [{ type: "text", text: `No ${state} cron entries found matching "${match}"` }] };
|
|
1776
1894
|
}
|
|
1777
1895
|
const escapedCrontab = updated.join("\n").replace(/'/g, "'\\''");
|
|
1778
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user}
|
|
1896
|
+
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
1779
1897
|
if (result.exitCode !== 0) {
|
|
1780
1898
|
throw new Error(result.stderr || "Failed to update crontab");
|
|
1781
1899
|
}
|