@deinossrl/dgp-agent 1.4.16 → 1.4.20

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