@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.
Files changed (2) hide show
  1. package/index.mjs +287 -5
  2. 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.17';
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
- await shellAsync(`rsync -avz --delete dist/ ${ssh_user}@${server_host}:${deployFolder}`);
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
- await shellAsync(`ssh ${ssh_user}@${server_host} "sudo nginx -t && sudo systemctl reload nginx"`);
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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deinossrl/dgp-agent",
3
- "version": "1.4.17",
3
+ "version": "1.4.21",
4
4
  "description": "Agente local para Despliegue-GPT - Reporta el estado del repositorio Git a la plataforma TenMinute IA",
5
5
  "main": "index.mjs",
6
6
  "bin": {