@deinossrl/dgp-agent 1.0.0 → 1.2.5

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 +345 -22
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * DGP Agent - Agente local para Despliegue-GPT
4
- * @deinos/dgp-agent
4
+ * @deinossrl/dgp-agent
5
5
  *
6
6
  * Este agente corre en la máquina del desarrollador y:
7
7
  * 1. Reporta el estado del repositorio git a la plataforma TenMinute IA
8
- * 2. (Futuro) Ejecuta comandos enviados desde la plataforma
8
+ * 2. Ejecuta comandos de deploy enviados desde la plataforma
9
9
  *
10
10
  * Instalación:
11
- * npm install -g @deinos/dgp-agent
11
+ * npm install -g @deinossrl/dgp-agent
12
12
  *
13
13
  * Uso:
14
14
  * dgp-agent # Inicia el agente (reporta cada 30s)
15
15
  * dgp-agent status # Muestra estado una vez
16
+ * dgp-agent deploy # Modo deploy (escucha comandos)
16
17
  * dgp-agent help # Muestra ayuda
17
18
  *
18
19
  * Variables de entorno:
@@ -22,13 +23,16 @@
22
23
  * DGP_MACHINE_ID ID personalizado de la máquina
23
24
  */
24
25
 
25
- import { execSync } from 'child_process';
26
+ import { execSync, spawn } from 'child_process';
26
27
  import { hostname } from 'os';
27
28
 
28
29
  // Configuración
29
30
  const CONFIG = {
30
31
  apiUrl: process.env.DGP_API_URL || 'https://asivayhbrqennwiwttds.supabase.co/functions/v1/dgp-agent-status',
32
+ commandsUrl: process.env.DGP_COMMANDS_URL || 'https://asivayhbrqennwiwttds.supabase.co/rest/v1/dgp_agent_commands',
33
+ supabaseKey: process.env.DGP_SUPABASE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFzaXZheWhicnFlbm53aXd0dGRzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjczMDAwOTcsImV4cCI6MjA4Mjg3NjA5N30.s3a7dR-dPkEXI7B2lUTUXU69923hhuX6meheNeo5EKA',
31
34
  interval: parseInt(process.env.DGP_INTERVAL || '30', 10),
35
+ commandPollInterval: parseInt(process.env.DGP_COMMAND_POLL_INTERVAL || '10', 10),
32
36
  machineId: process.env.DGP_MACHINE_ID || `${hostname()}-${process.env.USERNAME || process.env.USER || 'dev'}`,
33
37
  authToken: process.env.DGP_AUTH_TOKEN || null,
34
38
  };
@@ -42,6 +46,7 @@ const colors = {
42
46
  red: '\x1b[31m',
43
47
  gray: '\x1b[90m',
44
48
  cyan: '\x1b[36m',
49
+ magenta: '\x1b[35m',
45
50
  bold: '\x1b[1m',
46
51
  };
47
52
 
@@ -62,6 +67,10 @@ function logInfo(message) {
62
67
  log(message, 'blue');
63
68
  }
64
69
 
70
+ function logCommand(message) {
71
+ log(message, 'magenta');
72
+ }
73
+
65
74
  /**
66
75
  * Ejecuta un comando git y retorna el resultado
67
76
  */
@@ -76,6 +85,64 @@ function git(command) {
76
85
  }
77
86
  }
78
87
 
88
+ /**
89
+ * Ejecuta un comando shell y retorna el resultado
90
+ */
91
+ function shell(command, options = {}) {
92
+ try {
93
+ return execSync(command, {
94
+ encoding: 'utf-8',
95
+ stdio: ['pipe', 'pipe', 'pipe'],
96
+ ...options,
97
+ }).trim();
98
+ } catch (error) {
99
+ throw new Error(`Command failed: ${command}\n${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Ejecuta un comando shell de forma asíncrona con output en tiempo real
105
+ */
106
+ function shellAsync(command, options = {}) {
107
+ return new Promise((resolve, reject) => {
108
+ const isWindows = process.platform === 'win32';
109
+ const shellCmd = isWindows ? 'cmd' : '/bin/sh';
110
+ const shellArgs = isWindows ? ['/c', command] : ['-c', command];
111
+
112
+ const proc = spawn(shellCmd, shellArgs, {
113
+ ...options,
114
+ stdio: ['inherit', 'pipe', 'pipe'],
115
+ });
116
+
117
+ let stdout = '';
118
+ let stderr = '';
119
+
120
+ proc.stdout.on('data', (data) => {
121
+ const str = data.toString();
122
+ stdout += str;
123
+ process.stdout.write(colors.gray + str + colors.reset);
124
+ });
125
+
126
+ proc.stderr.on('data', (data) => {
127
+ const str = data.toString();
128
+ stderr += str;
129
+ process.stderr.write(colors.yellow + str + colors.reset);
130
+ });
131
+
132
+ proc.on('close', (code) => {
133
+ if (code === 0) {
134
+ resolve({ stdout, stderr, code });
135
+ } else {
136
+ reject(new Error(`Command exited with code ${code}\n${stderr}`));
137
+ }
138
+ });
139
+
140
+ proc.on('error', (error) => {
141
+ reject(error);
142
+ });
143
+ });
144
+ }
145
+
79
146
  /**
80
147
  * Verifica si estamos en un repositorio git
81
148
  */
@@ -169,15 +236,20 @@ async function reportStatus(status) {
169
236
  const payload = {
170
237
  machine_id: CONFIG.machineId,
171
238
  timestamp: new Date().toISOString(),
239
+ agent_version: '1.2.5',
172
240
  status,
173
241
  };
174
242
 
175
243
  const headers = {
176
244
  'Content-Type': 'application/json',
245
+ 'apikey': CONFIG.supabaseKey,
177
246
  };
178
247
 
248
+ // Usar auth token del usuario si existe, sino usar el anon key
179
249
  if (CONFIG.authToken) {
180
250
  headers['Authorization'] = `Bearer ${CONFIG.authToken}`;
251
+ } else {
252
+ headers['Authorization'] = `Bearer ${CONFIG.supabaseKey}`;
181
253
  }
182
254
 
183
255
  const response = await fetch(CONFIG.apiUrl, {
@@ -194,6 +266,195 @@ async function reportStatus(status) {
194
266
  return await response.json();
195
267
  }
196
268
 
269
+ /**
270
+ * Obtiene comandos pendientes de la plataforma
271
+ */
272
+ async function getPendingCommands() {
273
+ const url = `${CONFIG.commandsUrl}?status=eq.pending&select=*&order=created_at.asc&limit=1`;
274
+
275
+ const response = await fetch(url, {
276
+ headers: {
277
+ 'apikey': CONFIG.supabaseKey,
278
+ 'Authorization': `Bearer ${CONFIG.supabaseKey}`,
279
+ },
280
+ });
281
+
282
+ if (!response.ok) {
283
+ throw new Error(`Failed to get commands: HTTP ${response.status}`);
284
+ }
285
+
286
+ return await response.json();
287
+ }
288
+
289
+ /**
290
+ * Actualiza el estado de un comando
291
+ */
292
+ async function updateCommandStatus(commandId, status, result = {}, errorMessage = null) {
293
+ const payload = {
294
+ status,
295
+ result,
296
+ error_message: errorMessage,
297
+ ...(status === 'running' ? { picked_up_at: new Date().toISOString() } : {}),
298
+ ...(status === 'success' || status === 'failed' ? { completed_at: new Date().toISOString() } : {}),
299
+ };
300
+
301
+ const url = `${CONFIG.commandsUrl}?id=eq.${commandId}`;
302
+
303
+ const response = await fetch(url, {
304
+ method: 'PATCH',
305
+ headers: {
306
+ 'apikey': CONFIG.supabaseKey,
307
+ 'Authorization': `Bearer ${CONFIG.supabaseKey}`,
308
+ 'Content-Type': 'application/json',
309
+ 'Prefer': 'return=minimal',
310
+ },
311
+ body: JSON.stringify(payload),
312
+ });
313
+
314
+ if (!response.ok) {
315
+ throw new Error(`Failed to update command: HTTP ${response.status}`);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Ejecuta un comando de deploy
321
+ */
322
+ async function executeDeploy(command) {
323
+ const { id, environment, branch, server_host, ssh_user, deploy_url } = command;
324
+
325
+ logCommand(`=== Executing Deploy Command ===`);
326
+ logInfo(`Environment: ${environment}`);
327
+ logInfo(`Branch: ${branch}`);
328
+ logInfo(`Server: ${ssh_user}@${server_host}`);
329
+
330
+ const steps = [];
331
+ let currentStep = '';
332
+
333
+ try {
334
+ // Mark as running
335
+ await updateCommandStatus(id, 'running');
336
+
337
+ // Step 1: Git fetch and checkout
338
+ currentStep = 'git_checkout';
339
+ logCommand(`[1/5] Checkout branch ${branch}...`);
340
+ steps.push({ step: currentStep, status: 'running' });
341
+
342
+ await shellAsync(`git fetch origin`);
343
+ await shellAsync(`git checkout ${branch}`);
344
+ await shellAsync(`git pull origin ${branch}`);
345
+
346
+ steps[steps.length - 1].status = 'success';
347
+ logSuccess(`Branch ${branch} updated`);
348
+
349
+ // Step 2: Install dependencies
350
+ currentStep = 'npm_install';
351
+ logCommand(`[2/5] Installing dependencies...`);
352
+ steps.push({ step: currentStep, status: 'running' });
353
+
354
+ await shellAsync(`npm ci`);
355
+
356
+ steps[steps.length - 1].status = 'success';
357
+ logSuccess(`Dependencies installed`);
358
+
359
+ // Step 3: Build
360
+ currentStep = 'npm_build';
361
+ logCommand(`[3/5] Building application...`);
362
+ steps.push({ step: currentStep, status: 'running' });
363
+
364
+ await shellAsync(`npm run build`);
365
+
366
+ steps[steps.length - 1].status = 'success';
367
+ logSuccess(`Build completed`);
368
+
369
+ // Step 4: Deploy via rsync
370
+ currentStep = 'rsync_deploy';
371
+ logCommand(`[4/5] Deploying to server...`);
372
+ steps.push({ step: currentStep, status: 'running' });
373
+
374
+ const deployFolder = environment === 'production'
375
+ ? '/var/www/tenminuteia-prod/'
376
+ : '/var/www/tenminuteia-staging/';
377
+
378
+ // Rsync the dist folder
379
+ await shellAsync(`rsync -avz --delete dist/ ${ssh_user}@${server_host}:${deployFolder}`);
380
+
381
+ steps[steps.length - 1].status = 'success';
382
+ logSuccess(`Files deployed to ${server_host}:${deployFolder}`);
383
+
384
+ // Step 5: Reload nginx
385
+ currentStep = 'reload_nginx';
386
+ logCommand(`[5/5] Reloading Nginx...`);
387
+ steps.push({ step: currentStep, status: 'running' });
388
+
389
+ await shellAsync(`ssh ${ssh_user}@${server_host} "sudo nginx -t && sudo systemctl reload nginx"`);
390
+
391
+ steps[steps.length - 1].status = 'success';
392
+ logSuccess(`Nginx reloaded`);
393
+
394
+ // Healthcheck (optional)
395
+ if (deploy_url) {
396
+ logCommand(`Verifying deployment at ${deploy_url}...`);
397
+ try {
398
+ const healthResponse = await fetch(deploy_url);
399
+ if (healthResponse.ok) {
400
+ logSuccess(`Healthcheck passed`);
401
+ } else {
402
+ log(`Healthcheck returned ${healthResponse.status}`, 'yellow');
403
+ }
404
+ } catch (e) {
405
+ log(`Healthcheck failed: ${e.message}`, 'yellow');
406
+ }
407
+ }
408
+
409
+ // Mark as success
410
+ await updateCommandStatus(id, 'success', { steps, deploy_url });
411
+
412
+ console.log('');
413
+ logSuccess(`=== Deploy to ${environment} completed successfully! ===`);
414
+ console.log('');
415
+
416
+ return { success: true };
417
+
418
+ } catch (error) {
419
+ logError(`Deploy failed at step: ${currentStep}`);
420
+ logError(error.message);
421
+
422
+ steps[steps.length - 1].status = 'failed';
423
+ steps[steps.length - 1].error = error.message;
424
+
425
+ await updateCommandStatus(id, 'failed', { steps }, error.message);
426
+
427
+ return { success: false, error: error.message };
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Ejecuta un comando recibido
433
+ */
434
+ async function executeCommand(command) {
435
+ logCommand(`Received command: ${command.command}`);
436
+
437
+ switch (command.command) {
438
+ case 'deploy':
439
+ return await executeDeploy(command);
440
+
441
+ case 'status':
442
+ const status = getRepoStatus();
443
+ await updateCommandStatus(command.id, 'success', { status });
444
+ return { success: true, status };
445
+
446
+ case 'rollback':
447
+ logError('Rollback not implemented yet');
448
+ await updateCommandStatus(command.id, 'failed', {}, 'Rollback not implemented');
449
+ return { success: false };
450
+
451
+ default:
452
+ logError(`Unknown command: ${command.command}`);
453
+ await updateCommandStatus(command.id, 'failed', {}, `Unknown command: ${command.command}`);
454
+ return { success: false };
455
+ }
456
+ }
457
+
197
458
  /**
198
459
  * Muestra el estado en consola
199
460
  */
@@ -223,11 +484,11 @@ function printStatus(status) {
223
484
  /**
224
485
  * Loop principal del agente
225
486
  */
226
- async function runAgent() {
487
+ async function runAgent(deployMode = false) {
227
488
  console.log('');
228
489
  console.log(`${colors.green}╔═══════════════════════════════════════════════════════╗${colors.reset}`);
229
490
  console.log(`${colors.green}║ DGP Agent - Despliegue-GPT Local Agent ║${colors.reset}`);
230
- console.log(`${colors.green}║ @deinos/dgp-agent v1.0.0 ║${colors.reset}`);
491
+ console.log(`${colors.green}║ @deinossrl/dgp-agent v1.2.5 ║${colors.reset}`);
231
492
  console.log(`${colors.green}╚═══════════════════════════════════════════════════════╝${colors.reset}`);
232
493
  console.log('');
233
494
 
@@ -240,6 +501,11 @@ async function runAgent() {
240
501
  logInfo(`Report interval: ${CONFIG.interval}s`);
241
502
  logInfo(`API URL: ${CONFIG.apiUrl}`);
242
503
 
504
+ if (deployMode) {
505
+ logInfo(`Deploy mode: ACTIVE`);
506
+ logInfo(`Command poll interval: ${CONFIG.commandPollInterval}s`);
507
+ }
508
+
243
509
  if (!CONFIG.authToken) {
244
510
  log('INFO: No auth token. Reporting anonymously.', 'yellow');
245
511
  log('Set DGP_AUTH_TOKEN to associate with your user.', 'gray');
@@ -249,7 +515,8 @@ async function runAgent() {
249
515
  logInfo('Press Ctrl+C to stop');
250
516
  console.log('');
251
517
 
252
- const runCycle = async () => {
518
+ // Status reporting cycle
519
+ const runStatusCycle = async () => {
253
520
  try {
254
521
  const status = getRepoStatus();
255
522
  printStatus(status);
@@ -262,8 +529,36 @@ async function runAgent() {
262
529
  }
263
530
  };
264
531
 
265
- await runCycle();
266
- setInterval(runCycle, CONFIG.interval * 1000);
532
+ // Command polling cycle (only in deploy mode)
533
+ const runCommandCycle = async () => {
534
+ try {
535
+ const commands = await getPendingCommands();
536
+ if (commands.length > 0) {
537
+ const command = commands[0];
538
+ logCommand(`Found pending command: ${command.id}`);
539
+ await executeCommand(command);
540
+ }
541
+ } catch (error) {
542
+ // Silent fail for command polling - don't spam logs
543
+ if (error.message.includes('42P01')) {
544
+ // Table doesn't exist yet - ignore
545
+ } else {
546
+ logError(`Command poll failed: ${error.message}`);
547
+ }
548
+ }
549
+ };
550
+
551
+ // Initial run
552
+ await runStatusCycle();
553
+ if (deployMode) {
554
+ await runCommandCycle();
555
+ }
556
+
557
+ // Set intervals
558
+ setInterval(runStatusCycle, CONFIG.interval * 1000);
559
+ if (deployMode) {
560
+ setInterval(runCommandCycle, CONFIG.commandPollInterval * 1000);
561
+ }
267
562
  }
268
563
 
269
564
  /**
@@ -309,37 +604,60 @@ async function showStatus() {
309
604
  function showHelp() {
310
605
  console.log(`
311
606
  ${colors.bold}${colors.cyan}DGP Agent - Despliegue-GPT Local Agent${colors.reset}
312
- ${colors.gray}@deinos/dgp-agent v1.0.0${colors.reset}
607
+ ${colors.gray}@deinossrl/dgp-agent v1.2.5${colors.reset}
313
608
 
314
609
  ${colors.bold}DESCRIPCIÓN${colors.reset}
315
610
  Agente local que reporta el estado de tu repositorio Git
316
- a la plataforma TenMinute IA (Despliegue-GPT).
611
+ a la plataforma TenMinute IA (Despliegue-GPT) y puede
612
+ ejecutar comandos de deploy remotos.
317
613
 
318
614
  ${colors.bold}INSTALACIÓN${colors.reset}
319
- ${colors.green}npm install -g @deinos/dgp-agent${colors.reset}
615
+ ${colors.green}npm install -g @deinossrl/dgp-agent${colors.reset}
320
616
 
321
617
  ${colors.bold}USO${colors.reset}
322
- ${colors.cyan}dgp-agent${colors.reset} Inicia el agente (reporta cada 30s)
618
+ ${colors.cyan}dgp-agent${colors.reset} Inicia el agente (solo reporta estado)
619
+ ${colors.cyan}dgp-agent deploy${colors.reset} Modo deploy (reporta + escucha comandos)
323
620
  ${colors.cyan}dgp-agent status${colors.reset} Muestra el estado actual una vez
324
621
  ${colors.cyan}dgp-agent help${colors.reset} Muestra esta ayuda
325
622
 
623
+ ${colors.bold}MODOS DE OPERACIÓN${colors.reset}
624
+ ${colors.yellow}Default${colors.reset} Solo reporta estado del repo cada 30s
625
+ ${colors.yellow}Deploy${colors.reset} Reporta estado + escucha comandos de la plataforma
626
+ Puede ejecutar: build, deploy via rsync, reload nginx
627
+
326
628
  ${colors.bold}VARIABLES DE ENTORNO${colors.reset}
327
- ${colors.yellow}DGP_AUTH_TOKEN${colors.reset} Token JWT para asociar a tu usuario
328
- (Obtenerlo desde la plataforma)
329
- ${colors.yellow}DGP_API_URL${colors.reset} URL del endpoint API
330
- ${colors.yellow}DGP_INTERVAL${colors.reset} Intervalo de reporte en segundos (default: 30)
331
- ${colors.yellow}DGP_MACHINE_ID${colors.reset} ID personalizado de la máquina
629
+ ${colors.yellow}DGP_AUTH_TOKEN${colors.reset} Token JWT para asociar a tu usuario
630
+ ${colors.yellow}DGP_API_URL${colors.reset} URL del endpoint API
631
+ ${colors.yellow}DGP_INTERVAL${colors.reset} Intervalo de reporte en segundos (default: 30)
632
+ ${colors.yellow}DGP_COMMAND_POLL_INTERVAL${colors.reset} Intervalo de polling comandos (default: 10)
633
+ ${colors.yellow}DGP_MACHINE_ID${colors.reset} ID personalizado de la máquina
332
634
 
333
635
  ${colors.bold}EJEMPLOS${colors.reset}
334
- # Iniciar agente básico
636
+ # Iniciar agente básico (solo reporta)
335
637
  ${colors.gray}$ dgp-agent${colors.reset}
336
638
 
639
+ # Iniciar en modo deploy (puede ejecutar deploys)
640
+ ${colors.gray}$ dgp-agent deploy${colors.reset}
641
+
337
642
  # Con token de autenticación
338
- ${colors.gray}$ DGP_AUTH_TOKEN=eyJ... dgp-agent${colors.reset}
643
+ ${colors.gray}$ DGP_AUTH_TOKEN=eyJ... dgp-agent deploy${colors.reset}
339
644
 
340
645
  # Intervalo personalizado (60 segundos)
341
646
  ${colors.gray}$ DGP_INTERVAL=60 dgp-agent${colors.reset}
342
647
 
648
+ ${colors.bold}REQUISITOS PARA DEPLOY${colors.reset}
649
+ - SSH key configurada para acceso al servidor
650
+ - Node.js y npm en el PATH
651
+ - rsync instalado (Linux/Mac) o equivalente (Windows)
652
+ - Permisos sudo para reload nginx (vía sudoers sin password)
653
+
654
+ ${colors.bold}CHANGELOG${colors.reset}
655
+ ${colors.cyan}v1.2.5${colors.reset} - Sincronización de versiones y changelog
656
+ ${colors.cyan}v1.2.4${colors.reset} - Mejoras en ejecución async de comandos shell
657
+ ${colors.cyan}v1.2.0${colors.reset} - Modo deploy con polling de comandos remotos
658
+ ${colors.cyan}v1.1.0${colors.reset} - Soporte para deploy via rsync y reload nginx
659
+ ${colors.cyan}v1.0.0${colors.reset} - Versión inicial: reporte de estado git
660
+
343
661
  ${colors.bold}MÁS INFO${colors.reset}
344
662
  https://github.com/DEINOS-SRL/tenminuteia
345
663
  `);
@@ -350,6 +668,11 @@ const args = process.argv.slice(2);
350
668
  const command = args[0];
351
669
 
352
670
  switch (command) {
671
+ case 'deploy':
672
+ case '-d':
673
+ case '--deploy':
674
+ runAgent(true); // Deploy mode
675
+ break;
353
676
  case 'status':
354
677
  case '-s':
355
678
  case '--status':
@@ -363,8 +686,8 @@ switch (command) {
363
686
  case 'version':
364
687
  case '-v':
365
688
  case '--version':
366
- console.log('@deinos/dgp-agent v1.0.0');
689
+ console.log('@deinossrl/dgp-agent v1.2.5');
367
690
  break;
368
691
  default:
369
- runAgent();
692
+ runAgent(false); // Status-only mode
370
693
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deinossrl/dgp-agent",
3
- "version": "1.0.0",
3
+ "version": "1.2.5",
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": {