@deinossrl/dgp-agent 1.4.17 → 1.4.21
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/index.mjs +287 -5
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -203,7 +203,7 @@ async function fetchPlatformConfig() {
|
|
|
203
203
|
let platformConfig = null;
|
|
204
204
|
|
|
205
205
|
// Versión del agente
|
|
206
|
-
const AGENT_VERSION = '1.4.
|
|
206
|
+
const AGENT_VERSION = '1.4.21';
|
|
207
207
|
let AGENT_MODE = 'smart'; // Siempre inteligente
|
|
208
208
|
|
|
209
209
|
// Configuración (prioridad: env vars > archivo config > platform config > defaults)
|
|
@@ -1017,12 +1017,30 @@ async function updateCommandStatus(commandId, status, result = {}, errorMessage
|
|
|
1017
1017
|
* Ejecuta un comando de deploy
|
|
1018
1018
|
*/
|
|
1019
1019
|
async function executeDeploy(command) {
|
|
1020
|
-
const { id, environment, branch, server_host, ssh_user, deploy_url } = command;
|
|
1020
|
+
const { id, environment, branch, server_host, ssh_user, deploy_url, params } = command;
|
|
1021
|
+
const sshPrivateKey = params?.ssh_private_key || null;
|
|
1021
1022
|
|
|
1022
1023
|
logCommand(`=== Executing Deploy Command ===`);
|
|
1023
1024
|
logInfo(`Environment: ${environment}`);
|
|
1024
1025
|
logInfo(`Branch: ${branch}`);
|
|
1025
1026
|
logInfo(`Server: ${ssh_user}@${server_host}`);
|
|
1027
|
+
if (sshPrivateKey) {
|
|
1028
|
+
logInfo(`SSH Key: Configurada desde plataforma`);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Si hay SSH key, guardarla en archivo temporal
|
|
1032
|
+
let sshKeyPath = null;
|
|
1033
|
+
let sshOptions = '';
|
|
1034
|
+
if (sshPrivateKey) {
|
|
1035
|
+
const os = await import('os');
|
|
1036
|
+
const fs = await import('fs');
|
|
1037
|
+
const path = await import('path');
|
|
1038
|
+
sshKeyPath = path.join(os.tmpdir(), `dgp_deploy_key_${Date.now()}`);
|
|
1039
|
+
// Normalizar saltos de línea (pueden venir como \n literal del JSON)
|
|
1040
|
+
const normalizedKey = sshPrivateKey.replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
|
|
1041
|
+
fs.writeFileSync(sshKeyPath, normalizedKey, { mode: 0o600 });
|
|
1042
|
+
sshOptions = `-e "ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no"`;
|
|
1043
|
+
}
|
|
1026
1044
|
|
|
1027
1045
|
const steps = [];
|
|
1028
1046
|
let currentStep = '';
|
|
@@ -1072,8 +1090,11 @@ async function executeDeploy(command) {
|
|
|
1072
1090
|
? '/var/www/tenminuteia-prod/'
|
|
1073
1091
|
: '/var/www/tenminuteia-staging/';
|
|
1074
1092
|
|
|
1075
|
-
// Rsync the dist folder
|
|
1076
|
-
|
|
1093
|
+
// Rsync the dist folder (con SSH key si disponible)
|
|
1094
|
+
const rsyncCmd = sshOptions
|
|
1095
|
+
? `rsync -avz --delete ${sshOptions} dist/ ${ssh_user}@${server_host}:${deployFolder}`
|
|
1096
|
+
: `rsync -avz --delete dist/ ${ssh_user}@${server_host}:${deployFolder}`;
|
|
1097
|
+
await shellAsync(rsyncCmd);
|
|
1077
1098
|
|
|
1078
1099
|
steps[steps.length - 1].status = 'success';
|
|
1079
1100
|
logSuccess(`Files deployed to ${server_host}:${deployFolder}`);
|
|
@@ -1083,7 +1104,11 @@ async function executeDeploy(command) {
|
|
|
1083
1104
|
logCommand(`[5/5] Reloading Nginx...`);
|
|
1084
1105
|
steps.push({ step: currentStep, status: 'running' });
|
|
1085
1106
|
|
|
1086
|
-
|
|
1107
|
+
// SSH con key si disponible
|
|
1108
|
+
const sshCmd = sshKeyPath
|
|
1109
|
+
? `ssh -i ${sshKeyPath} -o StrictHostKeyChecking=no ${ssh_user}@${server_host} "sudo nginx -t && sudo systemctl reload nginx"`
|
|
1110
|
+
: `ssh ${ssh_user}@${server_host} "sudo nginx -t && sudo systemctl reload nginx"`;
|
|
1111
|
+
await shellAsync(sshCmd);
|
|
1087
1112
|
|
|
1088
1113
|
steps[steps.length - 1].status = 'success';
|
|
1089
1114
|
logSuccess(`Nginx reloaded`);
|
|
@@ -1106,6 +1131,15 @@ async function executeDeploy(command) {
|
|
|
1106
1131
|
// Mark as success
|
|
1107
1132
|
await updateCommandStatus(id, 'success', { steps, deploy_url });
|
|
1108
1133
|
|
|
1134
|
+
// Limpiar SSH key temporal
|
|
1135
|
+
if (sshKeyPath) {
|
|
1136
|
+
try {
|
|
1137
|
+
const fs = await import('fs');
|
|
1138
|
+
fs.unlinkSync(sshKeyPath);
|
|
1139
|
+
logInfo('SSH key temporal eliminada');
|
|
1140
|
+
} catch (e) { /* ignorar */ }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1109
1143
|
console.log('');
|
|
1110
1144
|
logSuccess(`=== Deploy to ${environment} completed successfully! ===`);
|
|
1111
1145
|
console.log('');
|
|
@@ -1121,6 +1155,14 @@ async function executeDeploy(command) {
|
|
|
1121
1155
|
|
|
1122
1156
|
await updateCommandStatus(id, 'failed', { steps }, error.message);
|
|
1123
1157
|
|
|
1158
|
+
// Limpiar SSH key temporal
|
|
1159
|
+
if (sshKeyPath) {
|
|
1160
|
+
try {
|
|
1161
|
+
const fs = await import('fs');
|
|
1162
|
+
fs.unlinkSync(sshKeyPath);
|
|
1163
|
+
} catch (e) { /* ignorar */ }
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1124
1166
|
return { success: false, error: error.message };
|
|
1125
1167
|
}
|
|
1126
1168
|
}
|
|
@@ -1145,6 +1187,9 @@ async function executeCommand(command, useAI = false) {
|
|
|
1145
1187
|
case 'git_commit_push':
|
|
1146
1188
|
return await executeGitCommitPush(command, useAI);
|
|
1147
1189
|
|
|
1190
|
+
case 'test_connection':
|
|
1191
|
+
return await executeTestConnection(command);
|
|
1192
|
+
|
|
1148
1193
|
case 'rollback':
|
|
1149
1194
|
logError('Rollback not implemented yet');
|
|
1150
1195
|
await updateCommandStatus(command.id, 'failed', {}, 'Rollback not implemented');
|
|
@@ -1166,6 +1211,243 @@ async function executeCommand(command, useAI = false) {
|
|
|
1166
1211
|
}
|
|
1167
1212
|
}
|
|
1168
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* Analiza un error de conexión con IA y sugiere soluciones
|
|
1216
|
+
*/
|
|
1217
|
+
async function analyzeConnectionErrorWithAI(error, context) {
|
|
1218
|
+
if (!CONFIG.anthropicApiKey) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const systemPrompt = `Eres un experto en DevOps y administración de servidores.
|
|
1223
|
+
Analiza el error de conexión y proporciona:
|
|
1224
|
+
1. Un diagnóstico claro del problema
|
|
1225
|
+
2. Posibles causas
|
|
1226
|
+
3. Pasos para solucionarlo
|
|
1227
|
+
|
|
1228
|
+
Responde en español, de forma concisa y práctica.
|
|
1229
|
+
Responde SOLO con JSON válido:
|
|
1230
|
+
{
|
|
1231
|
+
"diagnosis": "explicación breve del problema",
|
|
1232
|
+
"causes": ["causa 1", "causa 2"],
|
|
1233
|
+
"solutions": ["solución 1", "solución 2"],
|
|
1234
|
+
"next_steps": "qué hacer primero"
|
|
1235
|
+
}`;
|
|
1236
|
+
|
|
1237
|
+
const prompt = `Error de conexión SSH:
|
|
1238
|
+
- Host: ${context.host}
|
|
1239
|
+
- Usuario: ${context.user}
|
|
1240
|
+
- Error: ${error}
|
|
1241
|
+
- Ping exitoso: ${context.pingOk ? 'Sí' : 'No'}
|
|
1242
|
+
- Latencia ping: ${context.pingLatency || 'N/A'}ms
|
|
1243
|
+
|
|
1244
|
+
Analiza este error y sugiere soluciones.`;
|
|
1245
|
+
|
|
1246
|
+
try {
|
|
1247
|
+
const response = await callClaude(prompt, systemPrompt);
|
|
1248
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1249
|
+
if (jsonMatch) {
|
|
1250
|
+
return JSON.parse(jsonMatch[0]);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (e) {
|
|
1253
|
+
// Silently fail
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Ejecuta test de conexión SSH a un servidor (con IA para diagnóstico)
|
|
1260
|
+
*/
|
|
1261
|
+
async function executeTestConnection(command) {
|
|
1262
|
+
const { server_host, ssh_user, params } = command;
|
|
1263
|
+
const sshPrivateKey = params?.ssh_private_key || null;
|
|
1264
|
+
|
|
1265
|
+
logCommand(`=== Testing SSH Connection ===`);
|
|
1266
|
+
logInfo(`Target: ${ssh_user || 'root'}@${server_host}`);
|
|
1267
|
+
|
|
1268
|
+
// Iniciar tracking de logs
|
|
1269
|
+
currentCommandId = command.id;
|
|
1270
|
+
commandLogs = [];
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
await updateCommandStatus(command.id, 'running', {});
|
|
1274
|
+
await addCommandLog('info', `Probando conexión a ${server_host}...`);
|
|
1275
|
+
|
|
1276
|
+
const results = {
|
|
1277
|
+
ping: { success: false, latency: null, error: null },
|
|
1278
|
+
ssh: { success: false, error: null, hostname: null, user: null },
|
|
1279
|
+
ai_diagnosis: null,
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
// Step 1: Ping test
|
|
1283
|
+
logInfo('Step 1: Testing network connectivity (ping)...');
|
|
1284
|
+
await addCommandLog('command', `ping ${server_host}`);
|
|
1285
|
+
|
|
1286
|
+
try {
|
|
1287
|
+
const isWindows = process.platform === 'win32';
|
|
1288
|
+
const pingCmd = isWindows ? `ping -n 2 -w 5000 ${server_host}` : `ping -c 2 -W 5 ${server_host}`;
|
|
1289
|
+
const pingResult = shellSync(pingCmd);
|
|
1290
|
+
|
|
1291
|
+
// Extraer latencia
|
|
1292
|
+
const latencyMatch = pingResult.match(/tiempo[=<](\d+)ms|time[=<](\d+(?:\.\d+)?)\s*ms/i);
|
|
1293
|
+
if (latencyMatch) {
|
|
1294
|
+
results.ping.latency = parseFloat(latencyMatch[1] || latencyMatch[2]);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
results.ping.success = true;
|
|
1298
|
+
logSuccess(`Ping OK (${results.ping.latency || '?'}ms)`);
|
|
1299
|
+
await addCommandLog('success', `Ping exitoso - Latencia: ${results.ping.latency || '?'}ms`);
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
results.ping.error = e.message;
|
|
1302
|
+
logError(`Ping failed: ${e.message}`);
|
|
1303
|
+
await addCommandLog('error', `Ping falló: ${e.message}`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Step 2: SSH test
|
|
1307
|
+
logInfo('Step 2: Testing SSH connection...');
|
|
1308
|
+
|
|
1309
|
+
// Si hay SSH key, guardarla en archivo temporal
|
|
1310
|
+
let sshKeyPath = null;
|
|
1311
|
+
if (sshPrivateKey) {
|
|
1312
|
+
const os = await import('os');
|
|
1313
|
+
const fs = await import('fs');
|
|
1314
|
+
const path = await import('path');
|
|
1315
|
+
sshKeyPath = path.join(os.tmpdir(), `dgp_test_key_${Date.now()}`);
|
|
1316
|
+
// Normalizar saltos de línea (pueden venir como \n literal del JSON)
|
|
1317
|
+
const normalizedKey = sshPrivateKey.replace(/\\n/g, '\n').replace(/\r\n/g, '\n');
|
|
1318
|
+
fs.writeFileSync(sshKeyPath, normalizedKey, { mode: 0o600 });
|
|
1319
|
+
logInfo('SSH Key: Usando clave desde plataforma');
|
|
1320
|
+
await addCommandLog('info', 'Usando SSH key configurada en la plataforma');
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const user = ssh_user || 'root';
|
|
1324
|
+
const sshOpts = sshKeyPath
|
|
1325
|
+
? `-i ${sshKeyPath} -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o BatchMode=yes`
|
|
1326
|
+
: `-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o BatchMode=yes`;
|
|
1327
|
+
|
|
1328
|
+
const sshCmd = `ssh ${sshOpts} ${user}@${server_host} "echo 'SSH_OK' && hostname && whoami"`;
|
|
1329
|
+
await addCommandLog('command', `ssh ${user}@${server_host} "hostname && whoami"`);
|
|
1330
|
+
|
|
1331
|
+
try {
|
|
1332
|
+
const sshResult = shellSync(sshCmd);
|
|
1333
|
+
const lines = sshResult.split('\n').filter(Boolean);
|
|
1334
|
+
|
|
1335
|
+
if (lines[0] === 'SSH_OK') {
|
|
1336
|
+
results.ssh.success = true;
|
|
1337
|
+
results.ssh.hostname = lines[1] || 'unknown';
|
|
1338
|
+
results.ssh.user = lines[2] || user;
|
|
1339
|
+
|
|
1340
|
+
logSuccess(`SSH OK - Connected as ${results.ssh.user}@${results.ssh.hostname}`);
|
|
1341
|
+
await addCommandLog('success', `Conexión SSH exitosa`);
|
|
1342
|
+
await addCommandLog('output', `Hostname: ${results.ssh.hostname}`);
|
|
1343
|
+
await addCommandLog('output', `Usuario: ${results.ssh.user}`);
|
|
1344
|
+
} else {
|
|
1345
|
+
results.ssh.error = 'Unexpected response';
|
|
1346
|
+
await addCommandLog('error', 'Respuesta inesperada del servidor');
|
|
1347
|
+
}
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
results.ssh.error = e.message;
|
|
1350
|
+
|
|
1351
|
+
// Analizar el error para dar mejor feedback
|
|
1352
|
+
let errorDescription = '';
|
|
1353
|
+
if (e.message.includes('Permission denied')) {
|
|
1354
|
+
errorDescription = 'Permiso denegado: La clave SSH no es válida o no está autorizada en el servidor';
|
|
1355
|
+
logError('SSH failed: Permission denied (clave SSH incorrecta o no autorizada)');
|
|
1356
|
+
} else if (e.message.includes('Connection timed out') || e.message.includes('timed out')) {
|
|
1357
|
+
errorDescription = 'Timeout: El puerto 22 está bloqueado o el servidor no es accesible por SSH';
|
|
1358
|
+
logError('SSH failed: Connection timed out (puerto 22 bloqueado o servidor no accesible)');
|
|
1359
|
+
} else if (e.message.includes('Connection refused')) {
|
|
1360
|
+
errorDescription = 'Conexión rechazada: SSH no está corriendo en el servidor';
|
|
1361
|
+
logError('SSH failed: Connection refused (SSH no está corriendo en el servidor)');
|
|
1362
|
+
} else if (e.message.includes('Host key verification failed')) {
|
|
1363
|
+
errorDescription = 'Verificación de host fallida';
|
|
1364
|
+
logError('SSH failed: Host key verification failed');
|
|
1365
|
+
} else {
|
|
1366
|
+
errorDescription = `Error SSH: ${e.message}`;
|
|
1367
|
+
logError(`SSH failed: ${e.message}`);
|
|
1368
|
+
}
|
|
1369
|
+
await addCommandLog('error', errorDescription);
|
|
1370
|
+
|
|
1371
|
+
// === IA: Analizar el error y sugerir soluciones ===
|
|
1372
|
+
if (CONFIG.anthropicApiKey) {
|
|
1373
|
+
logAI('Analizando error con IA...');
|
|
1374
|
+
await addCommandLog('ai', 'Analizando error con IA...');
|
|
1375
|
+
|
|
1376
|
+
const aiAnalysis = await analyzeConnectionErrorWithAI(e.message, {
|
|
1377
|
+
host: server_host,
|
|
1378
|
+
user: user,
|
|
1379
|
+
pingOk: results.ping.success,
|
|
1380
|
+
pingLatency: results.ping.latency,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
if (aiAnalysis) {
|
|
1384
|
+
results.ai_diagnosis = aiAnalysis;
|
|
1385
|
+
|
|
1386
|
+
logAI(`Diagnóstico: ${aiAnalysis.diagnosis}`);
|
|
1387
|
+
await addCommandLog('ai', `Diagnóstico: ${aiAnalysis.diagnosis}`);
|
|
1388
|
+
|
|
1389
|
+
if (aiAnalysis.causes && aiAnalysis.causes.length > 0) {
|
|
1390
|
+
logAI('Posibles causas:');
|
|
1391
|
+
await addCommandLog('ai', 'Posibles causas:');
|
|
1392
|
+
for (const cause of aiAnalysis.causes) {
|
|
1393
|
+
log(` • ${cause}`, 'cyan');
|
|
1394
|
+
await addCommandLog('ai', ` • ${cause}`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (aiAnalysis.solutions && aiAnalysis.solutions.length > 0) {
|
|
1399
|
+
logAI('Soluciones sugeridas:');
|
|
1400
|
+
await addCommandLog('ai', 'Soluciones sugeridas:');
|
|
1401
|
+
for (const solution of aiAnalysis.solutions) {
|
|
1402
|
+
log(` ✓ ${solution}`, 'green');
|
|
1403
|
+
await addCommandLog('ai', ` ✓ ${solution}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (aiAnalysis.next_steps) {
|
|
1408
|
+
logAI(`Siguiente paso: ${aiAnalysis.next_steps}`);
|
|
1409
|
+
await addCommandLog('ai', `Siguiente paso: ${aiAnalysis.next_steps}`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Limpiar SSH key temporal
|
|
1416
|
+
if (sshKeyPath) {
|
|
1417
|
+
try {
|
|
1418
|
+
const fs = await import('fs');
|
|
1419
|
+
fs.unlinkSync(sshKeyPath);
|
|
1420
|
+
} catch (e) { /* ignorar */ }
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Determinar estado final
|
|
1424
|
+
const overallSuccess = results.ssh.success;
|
|
1425
|
+
const status = overallSuccess ? 'completed' : 'failed';
|
|
1426
|
+
const errorMsg = overallSuccess ? null : (results.ssh.error || 'SSH connection failed');
|
|
1427
|
+
|
|
1428
|
+
await updateCommandStatus(command.id, status, results, errorMsg);
|
|
1429
|
+
|
|
1430
|
+
console.log('');
|
|
1431
|
+
if (overallSuccess) {
|
|
1432
|
+
logSuccess(`=== Connection Test PASSED ===`);
|
|
1433
|
+
} else {
|
|
1434
|
+
logError(`=== Connection Test FAILED ===`);
|
|
1435
|
+
if (results.ai_diagnosis) {
|
|
1436
|
+
log('Ver diagnóstico de IA arriba para posibles soluciones', 'yellow');
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
console.log('');
|
|
1440
|
+
|
|
1441
|
+
return { success: overallSuccess, results };
|
|
1442
|
+
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
logError(`Connection test failed: ${error.message}`);
|
|
1445
|
+
await addCommandLog('error', error.message);
|
|
1446
|
+
await updateCommandStatus(command.id, 'failed', {}, error.message);
|
|
1447
|
+
return { success: false, error: error.message };
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1169
1451
|
/**
|
|
1170
1452
|
* Ejecuta git add, commit y push (con mejoras de IA si está disponible)
|
|
1171
1453
|
*/
|