@deinossrl/dgp-agent 1.0.0 → 1.1.0

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 +333 -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
  */
@@ -194,6 +261,195 @@ async function reportStatus(status) {
194
261
  return await response.json();
195
262
  }
196
263
 
264
+ /**
265
+ * Obtiene comandos pendientes de la plataforma
266
+ */
267
+ async function getPendingCommands() {
268
+ const url = `${CONFIG.commandsUrl}?status=eq.pending&select=*&order=created_at.asc&limit=1`;
269
+
270
+ const response = await fetch(url, {
271
+ headers: {
272
+ 'apikey': CONFIG.supabaseKey,
273
+ 'Authorization': `Bearer ${CONFIG.supabaseKey}`,
274
+ },
275
+ });
276
+
277
+ if (!response.ok) {
278
+ throw new Error(`Failed to get commands: HTTP ${response.status}`);
279
+ }
280
+
281
+ return await response.json();
282
+ }
283
+
284
+ /**
285
+ * Actualiza el estado de un comando
286
+ */
287
+ async function updateCommandStatus(commandId, status, result = {}, errorMessage = null) {
288
+ const payload = {
289
+ status,
290
+ result,
291
+ error_message: errorMessage,
292
+ ...(status === 'running' ? { picked_up_at: new Date().toISOString() } : {}),
293
+ ...(status === 'success' || status === 'failed' ? { completed_at: new Date().toISOString() } : {}),
294
+ };
295
+
296
+ const url = `${CONFIG.commandsUrl}?id=eq.${commandId}`;
297
+
298
+ const response = await fetch(url, {
299
+ method: 'PATCH',
300
+ headers: {
301
+ 'apikey': CONFIG.supabaseKey,
302
+ 'Authorization': `Bearer ${CONFIG.supabaseKey}`,
303
+ 'Content-Type': 'application/json',
304
+ 'Prefer': 'return=minimal',
305
+ },
306
+ body: JSON.stringify(payload),
307
+ });
308
+
309
+ if (!response.ok) {
310
+ throw new Error(`Failed to update command: HTTP ${response.status}`);
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Ejecuta un comando de deploy
316
+ */
317
+ async function executeDeploy(command) {
318
+ const { id, environment, branch, server_host, ssh_user, deploy_url } = command;
319
+
320
+ logCommand(`=== Executing Deploy Command ===`);
321
+ logInfo(`Environment: ${environment}`);
322
+ logInfo(`Branch: ${branch}`);
323
+ logInfo(`Server: ${ssh_user}@${server_host}`);
324
+
325
+ const steps = [];
326
+ let currentStep = '';
327
+
328
+ try {
329
+ // Mark as running
330
+ await updateCommandStatus(id, 'running');
331
+
332
+ // Step 1: Git fetch and checkout
333
+ currentStep = 'git_checkout';
334
+ logCommand(`[1/5] Checkout branch ${branch}...`);
335
+ steps.push({ step: currentStep, status: 'running' });
336
+
337
+ await shellAsync(`git fetch origin`);
338
+ await shellAsync(`git checkout ${branch}`);
339
+ await shellAsync(`git pull origin ${branch}`);
340
+
341
+ steps[steps.length - 1].status = 'success';
342
+ logSuccess(`Branch ${branch} updated`);
343
+
344
+ // Step 2: Install dependencies
345
+ currentStep = 'npm_install';
346
+ logCommand(`[2/5] Installing dependencies...`);
347
+ steps.push({ step: currentStep, status: 'running' });
348
+
349
+ await shellAsync(`npm ci`);
350
+
351
+ steps[steps.length - 1].status = 'success';
352
+ logSuccess(`Dependencies installed`);
353
+
354
+ // Step 3: Build
355
+ currentStep = 'npm_build';
356
+ logCommand(`[3/5] Building application...`);
357
+ steps.push({ step: currentStep, status: 'running' });
358
+
359
+ await shellAsync(`npm run build`);
360
+
361
+ steps[steps.length - 1].status = 'success';
362
+ logSuccess(`Build completed`);
363
+
364
+ // Step 4: Deploy via rsync
365
+ currentStep = 'rsync_deploy';
366
+ logCommand(`[4/5] Deploying to server...`);
367
+ steps.push({ step: currentStep, status: 'running' });
368
+
369
+ const deployFolder = environment === 'production'
370
+ ? '/var/www/tenminuteia-prod/'
371
+ : '/var/www/tenminuteia-staging/';
372
+
373
+ // Rsync the dist folder
374
+ await shellAsync(`rsync -avz --delete dist/ ${ssh_user}@${server_host}:${deployFolder}`);
375
+
376
+ steps[steps.length - 1].status = 'success';
377
+ logSuccess(`Files deployed to ${server_host}:${deployFolder}`);
378
+
379
+ // Step 5: Reload nginx
380
+ currentStep = 'reload_nginx';
381
+ logCommand(`[5/5] Reloading Nginx...`);
382
+ steps.push({ step: currentStep, status: 'running' });
383
+
384
+ await shellAsync(`ssh ${ssh_user}@${server_host} "sudo nginx -t && sudo systemctl reload nginx"`);
385
+
386
+ steps[steps.length - 1].status = 'success';
387
+ logSuccess(`Nginx reloaded`);
388
+
389
+ // Healthcheck (optional)
390
+ if (deploy_url) {
391
+ logCommand(`Verifying deployment at ${deploy_url}...`);
392
+ try {
393
+ const healthResponse = await fetch(deploy_url);
394
+ if (healthResponse.ok) {
395
+ logSuccess(`Healthcheck passed`);
396
+ } else {
397
+ log(`Healthcheck returned ${healthResponse.status}`, 'yellow');
398
+ }
399
+ } catch (e) {
400
+ log(`Healthcheck failed: ${e.message}`, 'yellow');
401
+ }
402
+ }
403
+
404
+ // Mark as success
405
+ await updateCommandStatus(id, 'success', { steps, deploy_url });
406
+
407
+ console.log('');
408
+ logSuccess(`=== Deploy to ${environment} completed successfully! ===`);
409
+ console.log('');
410
+
411
+ return { success: true };
412
+
413
+ } catch (error) {
414
+ logError(`Deploy failed at step: ${currentStep}`);
415
+ logError(error.message);
416
+
417
+ steps[steps.length - 1].status = 'failed';
418
+ steps[steps.length - 1].error = error.message;
419
+
420
+ await updateCommandStatus(id, 'failed', { steps }, error.message);
421
+
422
+ return { success: false, error: error.message };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Ejecuta un comando recibido
428
+ */
429
+ async function executeCommand(command) {
430
+ logCommand(`Received command: ${command.command}`);
431
+
432
+ switch (command.command) {
433
+ case 'deploy':
434
+ return await executeDeploy(command);
435
+
436
+ case 'status':
437
+ const status = getRepoStatus();
438
+ await updateCommandStatus(command.id, 'success', { status });
439
+ return { success: true, status };
440
+
441
+ case 'rollback':
442
+ logError('Rollback not implemented yet');
443
+ await updateCommandStatus(command.id, 'failed', {}, 'Rollback not implemented');
444
+ return { success: false };
445
+
446
+ default:
447
+ logError(`Unknown command: ${command.command}`);
448
+ await updateCommandStatus(command.id, 'failed', {}, `Unknown command: ${command.command}`);
449
+ return { success: false };
450
+ }
451
+ }
452
+
197
453
  /**
198
454
  * Muestra el estado en consola
199
455
  */
@@ -223,11 +479,11 @@ function printStatus(status) {
223
479
  /**
224
480
  * Loop principal del agente
225
481
  */
226
- async function runAgent() {
482
+ async function runAgent(deployMode = false) {
227
483
  console.log('');
228
484
  console.log(`${colors.green}╔═══════════════════════════════════════════════════════╗${colors.reset}`);
229
485
  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}`);
486
+ console.log(`${colors.green}║ @deinossrl/dgp-agent v1.1.0 ║${colors.reset}`);
231
487
  console.log(`${colors.green}╚═══════════════════════════════════════════════════════╝${colors.reset}`);
232
488
  console.log('');
233
489
 
@@ -240,6 +496,11 @@ async function runAgent() {
240
496
  logInfo(`Report interval: ${CONFIG.interval}s`);
241
497
  logInfo(`API URL: ${CONFIG.apiUrl}`);
242
498
 
499
+ if (deployMode) {
500
+ logInfo(`Deploy mode: ACTIVE`);
501
+ logInfo(`Command poll interval: ${CONFIG.commandPollInterval}s`);
502
+ }
503
+
243
504
  if (!CONFIG.authToken) {
244
505
  log('INFO: No auth token. Reporting anonymously.', 'yellow');
245
506
  log('Set DGP_AUTH_TOKEN to associate with your user.', 'gray');
@@ -249,7 +510,8 @@ async function runAgent() {
249
510
  logInfo('Press Ctrl+C to stop');
250
511
  console.log('');
251
512
 
252
- const runCycle = async () => {
513
+ // Status reporting cycle
514
+ const runStatusCycle = async () => {
253
515
  try {
254
516
  const status = getRepoStatus();
255
517
  printStatus(status);
@@ -262,8 +524,36 @@ async function runAgent() {
262
524
  }
263
525
  };
264
526
 
265
- await runCycle();
266
- setInterval(runCycle, CONFIG.interval * 1000);
527
+ // Command polling cycle (only in deploy mode)
528
+ const runCommandCycle = async () => {
529
+ try {
530
+ const commands = await getPendingCommands();
531
+ if (commands.length > 0) {
532
+ const command = commands[0];
533
+ logCommand(`Found pending command: ${command.id}`);
534
+ await executeCommand(command);
535
+ }
536
+ } catch (error) {
537
+ // Silent fail for command polling - don't spam logs
538
+ if (error.message.includes('42P01')) {
539
+ // Table doesn't exist yet - ignore
540
+ } else {
541
+ logError(`Command poll failed: ${error.message}`);
542
+ }
543
+ }
544
+ };
545
+
546
+ // Initial run
547
+ await runStatusCycle();
548
+ if (deployMode) {
549
+ await runCommandCycle();
550
+ }
551
+
552
+ // Set intervals
553
+ setInterval(runStatusCycle, CONFIG.interval * 1000);
554
+ if (deployMode) {
555
+ setInterval(runCommandCycle, CONFIG.commandPollInterval * 1000);
556
+ }
267
557
  }
268
558
 
269
559
  /**
@@ -309,37 +599,53 @@ async function showStatus() {
309
599
  function showHelp() {
310
600
  console.log(`
311
601
  ${colors.bold}${colors.cyan}DGP Agent - Despliegue-GPT Local Agent${colors.reset}
312
- ${colors.gray}@deinos/dgp-agent v1.0.0${colors.reset}
602
+ ${colors.gray}@deinossrl/dgp-agent v1.1.0${colors.reset}
313
603
 
314
604
  ${colors.bold}DESCRIPCIÓN${colors.reset}
315
605
  Agente local que reporta el estado de tu repositorio Git
316
- a la plataforma TenMinute IA (Despliegue-GPT).
606
+ a la plataforma TenMinute IA (Despliegue-GPT) y puede
607
+ ejecutar comandos de deploy remotos.
317
608
 
318
609
  ${colors.bold}INSTALACIÓN${colors.reset}
319
- ${colors.green}npm install -g @deinos/dgp-agent${colors.reset}
610
+ ${colors.green}npm install -g @deinossrl/dgp-agent${colors.reset}
320
611
 
321
612
  ${colors.bold}USO${colors.reset}
322
- ${colors.cyan}dgp-agent${colors.reset} Inicia el agente (reporta cada 30s)
613
+ ${colors.cyan}dgp-agent${colors.reset} Inicia el agente (solo reporta estado)
614
+ ${colors.cyan}dgp-agent deploy${colors.reset} Modo deploy (reporta + escucha comandos)
323
615
  ${colors.cyan}dgp-agent status${colors.reset} Muestra el estado actual una vez
324
616
  ${colors.cyan}dgp-agent help${colors.reset} Muestra esta ayuda
325
617
 
618
+ ${colors.bold}MODOS DE OPERACIÓN${colors.reset}
619
+ ${colors.yellow}Default${colors.reset} Solo reporta estado del repo cada 30s
620
+ ${colors.yellow}Deploy${colors.reset} Reporta estado + escucha comandos de la plataforma
621
+ Puede ejecutar: build, deploy via rsync, reload nginx
622
+
326
623
  ${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
624
+ ${colors.yellow}DGP_AUTH_TOKEN${colors.reset} Token JWT para asociar a tu usuario
625
+ ${colors.yellow}DGP_API_URL${colors.reset} URL del endpoint API
626
+ ${colors.yellow}DGP_INTERVAL${colors.reset} Intervalo de reporte en segundos (default: 30)
627
+ ${colors.yellow}DGP_COMMAND_POLL_INTERVAL${colors.reset} Intervalo de polling comandos (default: 10)
628
+ ${colors.yellow}DGP_MACHINE_ID${colors.reset} ID personalizado de la máquina
332
629
 
333
630
  ${colors.bold}EJEMPLOS${colors.reset}
334
- # Iniciar agente básico
631
+ # Iniciar agente básico (solo reporta)
335
632
  ${colors.gray}$ dgp-agent${colors.reset}
336
633
 
634
+ # Iniciar en modo deploy (puede ejecutar deploys)
635
+ ${colors.gray}$ dgp-agent deploy${colors.reset}
636
+
337
637
  # Con token de autenticación
338
- ${colors.gray}$ DGP_AUTH_TOKEN=eyJ... dgp-agent${colors.reset}
638
+ ${colors.gray}$ DGP_AUTH_TOKEN=eyJ... dgp-agent deploy${colors.reset}
339
639
 
340
640
  # Intervalo personalizado (60 segundos)
341
641
  ${colors.gray}$ DGP_INTERVAL=60 dgp-agent${colors.reset}
342
642
 
643
+ ${colors.bold}REQUISITOS PARA DEPLOY${colors.reset}
644
+ - SSH key configurada para acceso al servidor
645
+ - Node.js y npm en el PATH
646
+ - rsync instalado (Linux/Mac) o equivalente (Windows)
647
+ - Permisos sudo para reload nginx (vía sudoers sin password)
648
+
343
649
  ${colors.bold}MÁS INFO${colors.reset}
344
650
  https://github.com/DEINOS-SRL/tenminuteia
345
651
  `);
@@ -350,6 +656,11 @@ const args = process.argv.slice(2);
350
656
  const command = args[0];
351
657
 
352
658
  switch (command) {
659
+ case 'deploy':
660
+ case '-d':
661
+ case '--deploy':
662
+ runAgent(true); // Deploy mode
663
+ break;
353
664
  case 'status':
354
665
  case '-s':
355
666
  case '--status':
@@ -363,8 +674,8 @@ switch (command) {
363
674
  case 'version':
364
675
  case '-v':
365
676
  case '--version':
366
- console.log('@deinos/dgp-agent v1.0.0');
677
+ console.log('@deinossrl/dgp-agent v1.1.0');
367
678
  break;
368
679
  default:
369
- runAgent();
680
+ runAgent(false); // Status-only mode
370
681
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deinossrl/dgp-agent",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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": {