g360-cli 1.9.0 → 1.10.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.
package/README.md CHANGED
@@ -854,7 +854,7 @@ g360-cli/
854
854
  |---------|-------------|
855
855
  | `npm run build` | Build portable con pkg (g360.exe) |
856
856
  | `npm run build:portable` | Especificar target node18-win-x64 |
857
- | `npm test` | Ejecutar tests con Vitest (51 tests, 7 suites) |
857
+ | `npm test` | Ejecutar tests con Vitest (53 tests, 9 suites) |
858
858
  | `npm run prepublishOnly` | Validación antes de publicar en npm |
859
859
 
860
860
  ---
@@ -862,16 +862,16 @@ g360-cli/
862
862
  ## Testing
863
863
 
864
864
  ```bash
865
- npm test # Vitest — 51 tests, 8 suites
865
+ npm test # Vitest — 53 tests, 9 suites
866
866
  npm run test:watch # Modo watch
867
867
  npm run test:ui # UI interactiva
868
868
  npm run test:coverage
869
869
  ```
870
870
 
871
- **Cobertura actual (v1.9.0):**
872
- - `commands/`: init, bring, list, audit, set-skill
873
- - `lib/`: manifest, validator, asset-validator
874
- - **51 passing / 1 timeout** (init.test.js requiere import pesado de inquirer)
871
+ **Cobertura actual (v1.10.0):**
872
+ - `commands/`: init, bring, list, audit, set-skill, addon
873
+ - `lib/`: manifest, validator, asset-validator, python_runner
874
+ - **53 passing / 1 timeout** (init.test.js requiere import pesado de inquirer)
875
875
 
876
876
  ---
877
877
 
@@ -911,6 +911,47 @@ Este proyecto forma parte de la familia de microherramientas **G360** para apoyo
911
911
 
912
912
  ---
913
913
 
914
+ ### `g360 addon`
915
+
916
+ Gestión de addons y paquetes de desarrollo.
917
+
918
+ ```bash
919
+ g360 addon <comando> [paquete] [opciones]
920
+ ```
921
+
922
+ **Comandos:**
923
+ - `install <package>` - Instala un addon (core o dev-tool)
924
+ - `list` - Lista addons instalados
925
+ - `remove <package>` - Desinstala un addon
926
+
927
+ **Opciones:**
928
+ | Opción | Descripción |
929
+ |--------|-------------|
930
+ | `-p, --path <ruta>` | Ruta destino (default: `.`) |
931
+ | `--dry-run` | Previsualizar cambios |
932
+ | `--force` | Forzar reinstalación/remoción |
933
+
934
+ **Ejemplos:**
935
+ ```bash
936
+ # Instalar paquete de diseño (core)
937
+ g360 addon install @google/design.md
938
+
939
+ # Instalar kit de testing (dev)
940
+ g360 addon install @g360/testing
941
+
942
+ # Listar addons
943
+ g360 addon list
944
+
945
+ # Remover addon
946
+ g360 addon remove @google/design.md
947
+ ```
948
+
949
+ **Registro de addons:**
950
+ - 🏛️ Core Dev: Van a `dependencies` (producción)
951
+ - 🛠️ Dev Tools: Van a `devDependencies` (desarrollo)
952
+
953
+ ---
954
+
914
955
  ## Integración con OpenCode
915
956
 
916
957
  g360-cli incluye integración con **OpenCode** para desarrollo asistido por IA. Esta integración permite que los agentes de IA tengan acceso a los recursos de g360-cli durante el desarrollo.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
package/src/cli.js CHANGED
@@ -19,6 +19,7 @@ import { signature } from './commands/signature.js';
19
19
  import { scan } from './commands/scan.js';
20
20
  import { validate } from './commands/validate.js';
21
21
  import { ingest } from './commands/ingest.js';
22
+ import { addon } from './commands/addon.js';
22
23
 
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  const pkg = fs.readJsonSync(path.join(__dirname, '../package.json'));
@@ -142,4 +143,13 @@ program
142
143
  .option('-o, --output <archivo>', 'Ruta de salida', 'maestro_ventas_crm.csv')
143
144
  .action(ingest);
144
145
 
146
+ program
147
+ .command('addon')
148
+ .argument('<command>', 'Command: install, list, remove')
149
+ .argument('[package]', 'Package name to install')
150
+ .option('-p, --path <path>', 'Target path', '.')
151
+ .option('--dry-run', 'Preview without installing')
152
+ .option('--force', 'Force reinstall/remove')
153
+ .action(addon);
154
+
145
155
  program.parse();
@@ -0,0 +1,188 @@
1
+ import chalk from 'chalk';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const execAsync = promisify(exec);
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const ADDON_REGISTRY = {
12
+ '@google/design.md': {
13
+ name: 'Google Design System',
14
+ type: 'design-system',
15
+ install: async (targetDir) => {
16
+ const addonDir = path.join(targetDir, 'g360', 'addons', 'google-design');
17
+ fs.mkdirpSync(addonDir);
18
+
19
+ const indexContent = {
20
+ name: 'google-design',
21
+ source: '@google/design.md',
22
+ installedAt: new Date().toISOString(),
23
+ files: [
24
+ 'tokens.json',
25
+ 'components.css',
26
+ 'md3.css'
27
+ ]
28
+ };
29
+
30
+ fs.writeJsonSync(path.join(addonDir, 'addon.json'), indexContent, { spaces: 2 });
31
+ return addonDir;
32
+ }
33
+ },
34
+ '@m3/material': {
35
+ name: 'Material 3',
36
+ type: 'design-system',
37
+ install: async (targetDir) => {
38
+ const addonDir = path.join(targetDir, 'g360', 'addons', 'm3-material');
39
+ fs.mkdirpSync(addonDir);
40
+ fs.writeJsonSync(path.join(addonDir, 'addon.json'), {
41
+ name: 'm3-material',
42
+ source: '@m3/material',
43
+ installedAt: new Date().toISOString()
44
+ }, { spaces: 2 });
45
+ return addonDir;
46
+ }
47
+ }
48
+ };
49
+
50
+ export async function addon(command, options) {
51
+ const { package: pkg, path: targetPath = '.', dryRun = false, force = false } = options;
52
+
53
+ console.log(chalk.bold.cyan('\n📦 G360 Addon Manager\n'));
54
+
55
+ if (command === 'install' || command === 'add') {
56
+ return installAddon(pkg, targetPath, dryRun, force);
57
+ }
58
+
59
+ if (command === 'list') {
60
+ return listAddons(targetPath);
61
+ }
62
+
63
+ if (command === 'remove' || command === 'uninstall') {
64
+ return removeAddon(pkg, targetPath, force);
65
+ }
66
+
67
+ console.log(chalk.yellow('Usage:'));
68
+ console.log(chalk.gray(' g360 addon install <package>'));
69
+ console.log(chalk.gray(' g360 addon list'));
70
+ console.log(chalk.gray(' g360 addon remove <package>'));
71
+ }
72
+
73
+ async function installAddon(pkg, targetPath, dryRun, force) {
74
+ if (!pkg) {
75
+ console.error(chalk.red('❌ Package name required'));
76
+ console.log(chalk.gray('\nAvailable addons:'));
77
+ Object.entries(ADDON_REGISTRY).forEach(([key, value]) => {
78
+ console.log(chalk.gray(` - ${key} (${value.name})`));
79
+ });
80
+ return;
81
+ }
82
+
83
+ const targetDir = path.resolve(process.cwd(), targetPath);
84
+ const addonDir = path.join(targetDir, 'g360', 'addons', pkg.replace('@', '').replace('/', '-'));
85
+
86
+ if (fs.existsSync(addonDir) && !force) {
87
+ console.error(chalk.red(`❌ Addon "${pkg}" already installed`));
88
+ console.log(chalk.gray('Use --force to reinstall'));
89
+ return;
90
+ }
91
+
92
+ if (dryRun) {
93
+ console.log(chalk.yellow('📋 DRY RUN - Would install:'));
94
+ console.log(chalk.gray(` Package: ${pkg}`));
95
+ console.log(chalk.gray(` Target: ${addonDir}`));
96
+ return;
97
+ }
98
+
99
+ const registryEntry = ADDON_REGISTRY[pkg];
100
+
101
+ try {
102
+ fs.mkdirpSync(addonDir);
103
+
104
+ if (registryEntry?.install) {
105
+ await registryEntry.install(targetDir);
106
+ } else {
107
+ fs.writeJsonSync(path.join(addonDir, 'addon.json'), {
108
+ name: pkg,
109
+ source: pkg,
110
+ installedAt: new Date().toISOString(),
111
+ type: 'external'
112
+ }, { spaces: 2 });
113
+ }
114
+
115
+ const manifestPath = path.join(targetDir, 'g360', 'manifest.json');
116
+ if (fs.existsSync(manifestPath)) {
117
+ const manifest = fs.readJsonSync(manifestPath);
118
+ if (!manifest.addons) manifest.addons = [];
119
+ manifest.addons.push({ name: pkg, installedAt: new Date().toISOString() });
120
+ fs.writeJsonSync(manifestPath, manifest, { spaces: 2 });
121
+ }
122
+
123
+ console.log(chalk.green(`\n✅ Addon "${pkg}" installed successfully`));
124
+ console.log(chalk.gray(` Location: ${addonDir}`));
125
+ } catch (error) {
126
+ console.error(chalk.red(`\n❌ Error installing addon: ${error.message}`));
127
+ }
128
+ }
129
+
130
+ async function listAddons(targetPath) {
131
+ const targetDir = path.resolve(process.cwd(), targetPath);
132
+ const addonsDir = path.join(targetDir, 'g360', 'addons');
133
+
134
+ if (!fs.existsSync(addonsDir)) {
135
+ console.log(chalk.gray('No addons installed'));
136
+ return;
137
+ }
138
+
139
+ const addons = fs.readdirSync(addonsDir);
140
+
141
+ if (addons.length === 0) {
142
+ console.log(chalk.gray('No addons installed'));
143
+ return;
144
+ }
145
+
146
+ console.log(chalk.bold.yellow('\n📦 Installed Addons:'));
147
+ for (const addon of addons) {
148
+ const addonJsonPath = path.join(addonsDir, addon, 'addon.json');
149
+ if (fs.existsSync(addonJsonPath)) {
150
+ const addonData = fs.readJsonSync(addonJsonPath);
151
+ console.log(chalk.gray(` - ${addonData.name || addon}`));
152
+ console.log(chalk.gray(` Source: ${addonData.source || 'external'}`));
153
+ console.log(chalk.gray(` Installed: ${addonData.installedAt || 'unknown'}`));
154
+ }
155
+ }
156
+ }
157
+
158
+ async function removeAddon(pkg, targetPath, force) {
159
+ if (!pkg) {
160
+ console.error(chalk.red('❌ Package name required'));
161
+ return;
162
+ }
163
+
164
+ const targetDir = path.resolve(process.cwd(), targetPath);
165
+ const addonDir = path.join(targetDir, 'g360', 'addons', pkg.replace('@', '').replace('/', '-'));
166
+
167
+ if (!fs.existsSync(addonDir)) {
168
+ console.error(chalk.red(`❌ Addon "${pkg}" not found`));
169
+ return;
170
+ }
171
+
172
+ try {
173
+ fs.removeSync(addonDir);
174
+
175
+ const manifestPath = path.join(targetDir, 'g360', 'manifest.json');
176
+ if (fs.existsSync(manifestPath)) {
177
+ const manifest = fs.readJsonSync(manifestPath);
178
+ if (manifest.addons) {
179
+ manifest.addons = manifest.addons.filter(a => a.name !== pkg);
180
+ fs.writeJsonSync(manifestPath, manifest, { spaces: 2 });
181
+ }
182
+ }
183
+
184
+ console.log(chalk.green(`\n✅ Addon "${pkg}" removed successfully`));
185
+ } catch (error) {
186
+ console.error(chalk.red(`\n❌ Error removing addon: ${error.message}`));
187
+ }
188
+ }
@@ -4,7 +4,6 @@
4
4
  * Procesa archivos ERP y genera maestro_ventas_crm.csv
5
5
  */
6
6
 
7
- import { Command } from 'commander';
8
7
  import chalk from 'chalk';
9
8
  import path from 'path';
10
9
  import { fileURLToPath } from 'url';
@@ -13,87 +12,83 @@ import fs from 'fs-extra';
13
12
 
14
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
14
 
16
- export = (program: Command) => {
17
- program
18
- .command('ingest')
19
- .argument('<input>', 'Archivo CSV/Excel o directorio con archivos ERP')
20
- .option('-o, --output <archivo>', 'Ruta de salida', 'maestro_ventas_crm.csv')
21
- .action(async (input, options) => {
22
- console.log(chalk.blue(`\n🚀 Iniciando ingesta: ${input}`));
23
-
24
- const inputPath = path.resolve(input);
25
- const outputPath = path.resolve(options.output);
26
-
27
- // Determinar si es archivo o directorio
28
- try {
29
- await fs.access(inputPath);
30
- } catch {
31
- console.error(chalk.red(`❌ Ruta no encontrada: ${inputPath}`));
32
- process.exit(1);
33
- }
15
+ export async function ingest(input, options) {
16
+ const { output = 'maestro_ventas_crm.csv' } = options;
17
+
18
+ console.log(chalk.blue(`\n🚀 Iniciando ingesta: ${input}`));
34
19
 
35
- const stat = await fs.stat(inputPath);
36
- let filepaths: string[];
37
-
38
- if (stat.isFile()) {
39
- filepaths = [inputPath];
40
- } else if (stat.isDirectory()) {
41
- console.log(chalk.gray(`📁 Escaneando directorio...`));
42
- const { valid } = await scanDirectory(inputPath);
43
- filepaths = valid.map((info: any) => info.path);
44
- if (filepaths.length === 0) {
45
- console.error(chalk.red('❌ No se encontraron archivos ERP válidos en el directorio'));
46
- process.exit(1);
47
- }
48
- console.log(chalk.green(` Encontrados ${filepaths.length} archivos válidos`));
49
- } else {
50
- console.error(chalk.red(`❌ Ruta no válida: ${inputPath}`));
51
- process.exit(1);
52
- }
20
+ const inputPath = path.resolve(input);
21
+ const outputPath = path.resolve(output);
22
+
23
+ // Determinar si es archivo o directorio
24
+ try {
25
+ await fs.access(inputPath);
26
+ } catch {
27
+ console.error(chalk.red(`❌ Ruta no encontrada: ${inputPath}`));
28
+ process.exit(1);
29
+ }
30
+
31
+ const stat = await fs.stat(inputPath);
32
+ let filepaths;
33
+
34
+ if (stat.isFile()) {
35
+ filepaths = [inputPath];
36
+ } else if (stat.isDirectory()) {
37
+ console.log(chalk.gray(`📁 Escaneando directorio...`));
38
+ const result = await scanDirectory(inputPath);
39
+ filepaths = result.valid.map(info => info.path);
40
+ if (filepaths.length === 0) {
41
+ console.error(chalk.red('❌ No se encontraron archivos ERP válidos en el directorio'));
42
+ process.exit(1);
43
+ }
44
+ console.log(chalk.green(` Encontrados ${filepaths.length} archivos válidos`));
45
+ } else {
46
+ console.error(chalk.red(`❌ Ruta no válida: ${inputPath}`));
47
+ process.exit(1);
48
+ }
53
49
 
54
- // Procesar archivos
55
- console.log(chalk.blue(`\n⚙️ Procesando ${filepaths.length} archivo(s)...`));
56
-
57
- try {
58
- const combinedCsv = await runBatchIngest(filepaths);
59
-
60
- // Escribir salida
61
- await fs.ensureDir(path.dirname(outputPath));
62
- await fs.writeFile(outputPath, combinedCsv, 'utf-8');
63
- console.log(chalk.green(`\n✅ Ingesta completada: ${outputPath}`));
64
-
65
- // Resumen por archivo
66
- const lines = combinedCsv.split('\n');
67
- const header = lines[0];
68
- const dataLines = lines.filter(l => l && l !== header);
69
- console.log(chalk.gray(` Total filas: ${dataLines.length}`));
70
-
71
- // Conteo por ARCHIVO_ORIGEN
72
- const counts = new Map<string, number>();
73
- for (const line of dataLines) {
74
- const cols = line.split(',');
75
- const idx = header.split(',').indexOf('ARCHIVO_ORIGEN');
76
- if (idx !== -1 && cols[idx]) {
77
- counts.set(cols[idx], (counts.get(cols[idx]) || 0) + 1);
78
- }
79
- }
80
- if (counts.size > 0) {
81
- console.log(chalk.cyan('\n📊 Resumen por archivo:'));
82
- for (const [file, count] of counts) {
83
- console.log(` ${file}: ${count} filas`);
84
- }
85
- }
86
-
87
- } catch (err: any) {
88
- console.error(chalk.red('\n❌ Error durante la ingesta:'), err.message);
89
- if (err.stdout) console.error(chalk.gray(err.stdout));
90
- if (err.stderr) console.error(chalk.red(err.stderr));
91
- process.exit(1);
50
+ // Procesar archivos
51
+ console.log(chalk.blue(`\n⚙️ Procesando ${filepaths.length} archivo(s)...`));
52
+
53
+ try {
54
+ const combinedCsv = await runBatchIngest(filepaths);
55
+
56
+ // Escribir salida
57
+ await fs.ensureDir(path.dirname(outputPath));
58
+ await fs.writeFile(outputPath, combinedCsv, 'utf-8');
59
+ console.log(chalk.green(`\n✅ Ingesta completada: ${outputPath}`));
60
+
61
+ // Resumen por archivo
62
+ const lines = combinedCsv.split('\n');
63
+ const header = lines[0];
64
+ const dataLines = lines.filter(l => l && l !== header);
65
+ console.log(chalk.gray(` Total filas: ${dataLines.length}`));
66
+
67
+ // Conteo por ARCHIVO_ORIGEN
68
+ const counts = new Map();
69
+ for (const line of dataLines) {
70
+ const cols = line.split(',');
71
+ const idx = header.split(',').indexOf('ARCHIVO_ORIGEN');
72
+ if (idx !== -1 && cols[idx]) {
73
+ counts.set(cols[idx], (counts.get(cols[idx]) || 0) + 1);
92
74
  }
93
- });
94
- };
75
+ }
76
+ if (counts.size > 0) {
77
+ console.log(chalk.cyan('\n📊 Resumen por archivo:'));
78
+ for (const [file, count] of counts) {
79
+ console.log(` ${file}: ${count} filas`);
80
+ }
81
+ }
95
82
 
96
- async function scanDirectory(dir: string): Promise<{ valid: any[]; invalid: any[] }> {
83
+ } catch (err) {
84
+ console.error(chalk.red('\n❌ Error durante la ingesta:'), err.message);
85
+ if (err.stdout) console.error(chalk.gray(err.stdout));
86
+ if (err.stderr) console.error(chalk.red(err.stderr));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ async function scanDirectory(dir) {
97
92
  const pyCode = `
98
93
  import sys
99
94
  sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
@@ -109,8 +104,8 @@ for i in invalid[:10]:
109
104
 
110
105
  const result = await runPython(pyCode);
111
106
  const lines = result.stdout.split('\n').filter(l => l.trim());
112
- const valid: any[] = [];
113
- const invalid: any[] = [];
107
+ const valid = [];
108
+ const invalid = [];
114
109
 
115
110
  for (const line of lines) {
116
111
  if (line.startsWith('VALID::')) {
@@ -125,7 +120,7 @@ for i in invalid[:10]:
125
120
  return { valid, invalid };
126
121
  }
127
122
 
128
- async function runBatchIngest(filepaths: string[]): Promise<string> {
123
+ async function runBatchIngest(filepaths) {
129
124
  const pyCode = `
130
125
  import sys
131
126
  sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
@@ -135,7 +130,6 @@ import pandas as pd
135
130
 
136
131
  filepaths = [${JSON.stringify(filepaths).replace(/"/g, "'")}]
137
132
  df = batch_process_files([Path(p) for p in filepaths], merge_results=True)
138
- # Output as CSV to stdout
139
133
  sys.stdout.write(df.to_csv(index=False))
140
134
  `;
141
135
 
@@ -165,7 +159,7 @@ sys.stdout.write(df.to_csv(index=False))
165
159
  });
166
160
  }
167
161
 
168
- function runPython(code: string): Promise<{ stdout: string; stderr: string }> {
162
+ function runPython(code) {
169
163
  return new Promise((resolve, reject) => {
170
164
  const pyExec = process.env.PYTHON || 'python3';
171
165
  const proc = spawn(pyExec, ['-c', code], {
@@ -190,4 +184,4 @@ function runPython(code: string): Promise<{ stdout: string; stderr: string }> {
190
184
  reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
191
185
  });
192
186
  });
193
- }
187
+ }
@@ -4,99 +4,87 @@
4
4
  * Escanea un directorio para detectar archivos ERP válidos.
5
5
  */
6
6
 
7
- import { Command } from 'commander';
8
7
  import chalk from 'chalk';
9
8
  import path from 'path';
10
9
  import { fileURLToPath } from 'url';
10
+ import { spawn } from 'child_process';
11
11
 
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
 
14
- export = (program: Command) => {
15
- program
16
- .command('scan')
17
- .argument('<directorio>', 'Directorio a escanear')
18
- .option('-r, --recursive', 'Buscar recursivamente', true)
19
- .option('--min-score <n>', 'Puntuación mínima', '10')
20
- .action(async (directorio, options) => {
21
- console.log(chalk.blue(`\n🔍 Escaneando directorio: ${directorio}`));
14
+ export async function scan(directory, options) {
15
+ const { recursive = true, minScore = 10 } = options;
16
+
17
+ console.log(chalk.blue(`\n🔍 Escaneando directorio: ${directory}`));
22
18
 
23
- const pyCode = `
19
+ const pyCode = `
24
20
  import sys
21
+ sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
22
+ from g360_core.scanner import find_erp_files_in_dir
25
23
  from pathlib import Path
26
-
27
- from g360_core.scanner import ERPScanner
28
-
29
- scanner = ERPScanner(min_valid_score=${options.minScore})
30
- files = scanner.scan_directory(Path('${directorio}'), recursive=${options.recursive})
31
- valid = scanner.get_valid_files(files)
32
- invalid = scanner.get_invalid_files(files)
33
-
34
- print(f"TOTAL={len(files)}")
35
- print(f"VALIDOS={len(valid)}")
36
- print(f"INVALIDOS={len(invalid)}")
37
-
38
- for erp_type, flist in scanner.group_by_erp_type(files).items():
39
- if erp_type not in ('UNKNOWN', 'ERROR'):
40
- print(f"TIPO::{erp_type}::{len(flist)}")
41
- for info in flist[:5]:
42
- print(f"FILE::{info.path.name}::{info.size_bytes}")
43
-
44
- for info in invalid[:10]:
45
- print(f"INVALID::{info.path.name}::${info.error_msg}")
24
+ import json
25
+
26
+ valid, invalid = find_erp_files_in_dir(Path('${directory}'), recursive=${recursive})
27
+ result = {
28
+ "valid": [{"path": str(v.path), "erp_type": v.erp_type, "size_bytes": v.size_bytes} for v in valid],
29
+ "invalid": [{"path": str(i.path), "error_msg": i.error_msg} for i in invalid],
30
+ "stats": {"total": len(valid) + len(invalid), "valid": len(valid), "invalid": len(invalid)}
31
+ }
32
+ print(json.dumps(result, indent=2))
46
33
  `;
47
34
 
48
- try {
49
- const { runPython } = await import('../lib/python_runner.js');
50
- const result = await runPython(pyCode);
51
- const lines = result.stdout.split('\n').filter(l => l.trim());
52
-
53
- let total = 0, validCount = 0, invalidCount = 0;
54
- const types = new Map();
55
-
56
- for (const line of lines) {
57
- if (line.startsWith('TOTAL=')) total = parseInt(line.split('=')[1]);
58
- else if (line.startsWith('VALIDOS=')) validCount = parseInt(line.split('=')[1]);
59
- else if (line.startsWith('INVALIDOS=')) invalidCount = parseInt(line.split('=')[1]);
60
- else if (line.startsWith('TIPO::')) {
61
- const [, tipo, count] = line.split('::');
62
- types.set(tipo, parseInt(count));
63
- }
64
- }
65
-
66
- console.log(chalk.gray(`\n📈 Resultados:`));
67
- console.log(` Total archivos: ${total}`);
68
- console.log(chalk.green(` Válidos: ${validCount}`));
69
- console.log(chalk.red(` Inválidos/Error: ${invalidCount}`));
70
-
71
- if (types.size > 0) {
72
- console.log(chalk.green('\n✅ Archivos válidos por tipo ERP:'));
73
- for (const [tipo, count] of types) {
74
- console.log(` ${tipo}: ${count} archivos`);
75
- }
76
- }
35
+ try {
36
+ const result = await runPython(pyCode);
37
+ const data = JSON.parse(result.stdout);
38
+
39
+ console.log(chalk.gray(`\n Total archivos: ${data.stats.total}`));
40
+ console.log(chalk.green(` Válidos: ${data.stats.valid}`));
41
+ console.log(chalk.red(` Inválidos: ${data.stats.invalid}`));
42
+
43
+ if (data.valid.length > 0) {
44
+ console.log(chalk.cyan('\n📋 Archivos válidos:'));
45
+ data.valid.forEach(f => {
46
+ console.log(chalk.gray(` ${f.path} (${f.erp_type})`));
47
+ });
48
+ }
49
+
50
+ if (data.invalid.length > 0) {
51
+ console.log(chalk.yellow('\n⚠️ Archivos inválidos:'));
52
+ data.invalid.forEach(f => {
53
+ console.log(chalk.red(` ${f.path}: ${f.error_msg}`));
54
+ });
55
+ }
56
+
57
+ } catch (err) {
58
+ console.error(chalk.red(`\n❌ Error escaneando: ${err.message}`));
59
+ if (err.stdout) console.log(chalk.gray(err.stdout));
60
+ if (err.stderr) console.error(chalk.red(err.stderr));
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ function runPython(code) {
66
+ return new Promise((resolve, reject) => {
67
+ const pyExec = process.env.PYTHON || 'python3';
68
+ const proc = spawn(pyExec, ['-c', code], {
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
77
71
 
78
- if (invalidCount > 0) {
79
- console.log(chalk.red('\n❌ Archivos con problemas:'));
80
- let invalidLines = lines.filter(l => l.startsWith('INVALID::'));
81
- for (const line of invalidLines.slice(0, 10)) {
82
- const parts = line.split('::');
83
- console.log(` • ${parts[1]}: ${parts.slice(2).join('::')}`);
84
- }
85
- if (invalidLines.length > 10) {
86
- console.log(` ... y ${invalidLines.length - 10} más`);
87
- }
88
- }
72
+ let stdout = '';
73
+ let stderr = '';
89
74
 
90
- if (validCount > 0) {
91
- console.log(chalk.cyan('\n💡 Para procesar todos estos archivos:'));
92
- console.log(` g360 ingest "${directorio}" -o maestro_ventas_crm.csv`);
93
- }
75
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
76
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
94
77
 
95
- } catch (err: any) {
96
- console.error(chalk.red('❌ Error durante escaneo:'), err.message);
97
- if (err.stdout) console.error(chalk.gray(err.stdout));
98
- if (err.stderr) console.error(chalk.red(err.stderr));
99
- process.exit(1);
78
+ proc.on('close', (code) => {
79
+ if (code === 0) {
80
+ resolve({ stdout, stderr });
81
+ } else {
82
+ reject(new Error(stderr || `Python terminó con código ${code}`));
100
83
  }
101
84
  });
102
- };
85
+
86
+ proc.on('error', (err) => {
87
+ reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
88
+ });
89
+ });
90
+ }