g360-cli 1.7.1 → 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.
Files changed (54) hide show
  1. package/README.md +83 -8
  2. package/package.json +16 -6
  3. package/py/pyproject.toml +4 -4
  4. package/py/requirements.txt +4 -0
  5. package/py/src/g360_core/__init__.py +67 -4
  6. package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
  7. package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
  9. package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
  10. package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
  11. package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
  12. package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
  13. package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
  14. package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
  15. package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
  16. package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
  17. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
  18. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
  19. package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
  20. package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
  21. package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
  22. package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
  23. package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
  24. package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
  25. package/py/src/g360_core/batch_processor.py +120 -0
  26. package/py/src/g360_core/commercial_engine.py +305 -0
  27. package/py/src/g360_core/logger.py +40 -0
  28. package/py/src/g360_core/pipeline.py +578 -0
  29. package/py/src/g360_core/processor.py +634 -0
  30. package/py/src/g360_core/processor_segmentacion.py +859 -0
  31. package/py/src/g360_core/processor_sku.py +427 -0
  32. package/py/src/g360_core/scanner.py +218 -0
  33. package/py/src/g360_core/utils.py +435 -0
  34. package/src/cli.js +35 -2
  35. package/src/commands/addon.js +188 -0
  36. package/src/commands/ingest.js +187 -0
  37. package/src/commands/scan.js +90 -0
  38. package/src/commands/validate.js +150 -0
  39. package/src/lib/python_runner.js +89 -0
  40. package/py/src/g360_core/flet/__init__.py +0 -3
  41. package/py/src/g360_core/flet/ingestion_panel.py +0 -218
  42. package/py/src/g360_core/ingestion.py +0 -480
  43. package/src/assets/engine/g360-data-validator.js +0 -44
  44. package/src/assets/engine/g360-engine.js +0 -12
  45. package/src/assets/engine/g360-field-mapper.js +0 -35
  46. package/src/assets/engine/g360-skill-audit.mjs +0 -37
  47. package/src/assets/engine/g360-skill-meta-evaluator.mjs +0 -33
  48. package/src/lib/assets.js +0 -38
  49. package/src/lib/checksum.js +0 -27
  50. package/src/lib/config.js +0 -23
  51. package/src/lib/offline.js +0 -33
  52. package/src/lib/presenter.js +0 -24
  53. package/src/lib/rollback.js +0 -49
  54. package/src/lib/theme.js +0 -30
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comando: g360 ingest <archivo|directorio> [-o salida.csv]
4
+ * Procesa archivos ERP y genera maestro_ventas_crm.csv
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { spawn } from 'child_process';
11
+ import fs from 'fs-extra';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
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}`));
19
+
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
+ }
49
+
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);
74
+ }
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
+ }
82
+
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) {
92
+ const pyCode = `
93
+ import sys
94
+ sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
95
+ from g360_core.scanner import find_erp_files_in_dir
96
+ from pathlib import Path
97
+
98
+ valid, invalid = find_erp_files_in_dir(Path('${dir}'), recursive=True)
99
+ for v in valid:
100
+ print(f"VALID::{v.path.name}::{v.erp_type}::${v.size_bytes}")
101
+ for i in invalid[:10]:
102
+ print(f"INVALID::{i.path.name}::${i.error_msg}")
103
+ `;
104
+
105
+ const result = await runPython(pyCode);
106
+ const lines = result.stdout.split('\n').filter(l => l.trim());
107
+ const valid = [];
108
+ const invalid = [];
109
+
110
+ for (const line of lines) {
111
+ if (line.startsWith('VALID::')) {
112
+ const [, name, type, size] = line.split('::');
113
+ valid.push({ path: path.join(dir, name), erp_type: type, size_bytes: parseInt(size) });
114
+ } else if (line.startsWith('INVALID::')) {
115
+ const [, name, error] = line.split('::');
116
+ invalid.push({ path: path.join(dir, name), error_msg: error });
117
+ }
118
+ }
119
+
120
+ return { valid, invalid };
121
+ }
122
+
123
+ async function runBatchIngest(filepaths) {
124
+ const pyCode = `
125
+ import sys
126
+ sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
127
+ from g360_core.scanner import batch_process_files
128
+ from pathlib import Path
129
+ import pandas as pd
130
+
131
+ filepaths = [${JSON.stringify(filepaths).replace(/"/g, "'")}]
132
+ df = batch_process_files([Path(p) for p in filepaths], merge_results=True)
133
+ sys.stdout.write(df.to_csv(index=False))
134
+ `;
135
+
136
+ return new Promise((resolve, reject) => {
137
+ const pyExec = process.env.PYTHON || 'python3';
138
+ const proc = spawn(pyExec, ['-c', pyCode], {
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ });
141
+
142
+ let stdout = '';
143
+ let stderr = '';
144
+
145
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
146
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
147
+
148
+ proc.on('close', (code) => {
149
+ if (code === 0) {
150
+ resolve(stdout);
151
+ } else {
152
+ reject(new Error(stderr || `Python terminó con código ${code}`));
153
+ }
154
+ });
155
+
156
+ proc.on('error', (err) => {
157
+ reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
158
+ });
159
+ });
160
+ }
161
+
162
+ function runPython(code) {
163
+ return new Promise((resolve, reject) => {
164
+ const pyExec = process.env.PYTHON || 'python3';
165
+ const proc = spawn(pyExec, ['-c', code], {
166
+ stdio: ['ignore', 'pipe', 'pipe'],
167
+ });
168
+
169
+ let stdout = '';
170
+ let stderr = '';
171
+
172
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
173
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
174
+
175
+ proc.on('close', (code) => {
176
+ if (code === 0) {
177
+ resolve({ stdout, stderr });
178
+ } else {
179
+ reject(new Error(stderr || `Python terminó con código ${code}`));
180
+ }
181
+ });
182
+
183
+ proc.on('error', (err) => {
184
+ reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
185
+ });
186
+ });
187
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comando: g360 scan <directorio>
4
+ * Escanea un directorio para detectar archivos ERP válidos.
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { spawn } from 'child_process';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ export async function scan(directory, options) {
15
+ const { recursive = true, minScore = 10 } = options;
16
+
17
+ console.log(chalk.blue(`\n🔍 Escaneando directorio: ${directory}`));
18
+
19
+ const pyCode = `
20
+ import sys
21
+ sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
22
+ from g360_core.scanner import find_erp_files_in_dir
23
+ from pathlib import Path
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))
33
+ `;
34
+
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
+ });
71
+
72
+ let stdout = '';
73
+ let stderr = '';
74
+
75
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
76
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
77
+
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}`));
83
+ }
84
+ });
85
+
86
+ proc.on('error', (err) => {
87
+ reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
88
+ });
89
+ });
90
+ }
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Comando: g360 validate <archivos/directorios>
4
+ * Valida archivos ERP sin procesar completamente.
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { spawn } from 'child_process';
11
+ import fs from 'fs-extra';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ export async function validate(paths, options) {
16
+ const { recursive = false } = options;
17
+
18
+ console.log(chalk.blue('\n🔍 Validando archivos ERP\n'));
19
+
20
+ const filesToCheck = [];
21
+ for (const rawPath of paths) {
22
+ const p = path.resolve(rawPath);
23
+ try {
24
+ const stat = await fs.stat(p);
25
+ if (stat.isFile()) {
26
+ if (/\.(xls|xlsx|csv)$/i.test(p)) {
27
+ filesToCheck.push(p);
28
+ }
29
+ } else if (stat.isDirectory()) {
30
+ const pattern = recursive ? '**/*' : '*';
31
+ const extPattern = /\.(xls|xlsx|csv)$/i;
32
+ const entries = await fs.readdir(p, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const fullPath = path.join(p, entry.name);
35
+ if (entry.isFile() && extPattern.test(entry.name)) {
36
+ filesToCheck.push(fullPath);
37
+ } else if (entry.isDirectory() && recursive) {
38
+ // Simplificado: escaneo recursivo básico
39
+ const sub = await fs.readdir(fullPath, { withFileTypes: true });
40
+ for (const subEntry of sub) {
41
+ if (subEntry.isFile() && extPattern.test(subEntry.name)) {
42
+ filesToCheck.push(path.join(fullPath, subEntry.name));
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ } catch (err) {
49
+ console.warn(chalk.yellow(` ⚠ No se pudo acceder a ${p}: ${err.message}`));
50
+ }
51
+ }
52
+
53
+ if (filesToCheck.length === 0) {
54
+ console.warn(chalk.yellow(' No se encontraron archivos para validar'));
55
+ return;
56
+ }
57
+
58
+ const results = [];
59
+
60
+ for (const file of filesToCheck) {
61
+ try {
62
+ const validation = await validateSingleFile(file);
63
+ results.push(validation);
64
+ } catch (err) {
65
+ results.push({ path: file, valid: false, missing: [`ERROR: ${err.message}`] });
66
+ }
67
+ }
68
+
69
+ let validCount = 0;
70
+ for (const res of results) {
71
+ const status = res.valid ? chalk.green('✅') : chalk.red('❌');
72
+ const filename = path.basename(res.path);
73
+ console.log(`${status} ${filename} (${res.valid ? 'OK' : 'FALLÓ'})`);
74
+ if (!res.valid && res.missing.length > 0) {
75
+ console.log(chalk.gray(` Faltan: ${res.missing.join(', ')}`));
76
+ }
77
+ if (res.valid) validCount++;
78
+ }
79
+
80
+ console.log(chalk.gray(`\n📊 Resumen: ${validCount}/${results.length} archivos válidos`));
81
+ }
82
+
83
+ async function validateSingleFile(filepath) {
84
+ const pyCode = `
85
+ import sys
86
+ sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
87
+ from g360_core.scanner import ERPScanner
88
+ from pathlib import Path
89
+
90
+ scanner = ERPScanner(min_valid_score=10)
91
+ try:
92
+ df = scanner._read_headers(Path('${filepath.replace(/'/g, "\\'")}'), nrows=50)
93
+ missing = scanner._validate_columns(df)
94
+ is_valid = len(missing) == 0
95
+ print(f"VALID={is_valid}")
96
+ if missing:
97
+ print(f"MISSING={','.join(missing)}")
98
+ except Exception as e:
99
+ print(f"ERROR={str(e)}")
100
+ `;
101
+
102
+ try {
103
+ const result = await runPython(pyCode);
104
+ const lines = result.stdout.split('\n').filter(l => l.trim());
105
+
106
+ let valid = false;
107
+ let missing = [];
108
+
109
+ for (const line of lines) {
110
+ if (line.startsWith('VALID=')) {
111
+ valid = line.split('=')[1] === 'True';
112
+ } else if (line.startsWith('MISSING=')) {
113
+ missing = line.split('=')[1].split(',');
114
+ } else if (line.startsWith('ERROR=')) {
115
+ throw new Error(line.split('=')[1]);
116
+ }
117
+ }
118
+
119
+ return { path: filepath, valid, missing };
120
+ } catch (err) {
121
+ return { path: filepath, valid: false, missing: [err.message] };
122
+ }
123
+ }
124
+
125
+ function runPython(code) {
126
+ return new Promise((resolve, reject) => {
127
+ const pyExec = process.env.PYTHON || 'python3';
128
+ const proc = spawn(pyExec, ['-c', code], {
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ });
131
+
132
+ let stdout = '';
133
+ let stderr = '';
134
+
135
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
136
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
137
+
138
+ proc.on('close', (code) => {
139
+ if (code === 0) {
140
+ resolve({ stdout, stderr });
141
+ } else {
142
+ reject(new Error(stderr || `Python terminó con código ${code}`));
143
+ }
144
+ });
145
+
146
+ proc.on('error', (err) => {
147
+ reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
148
+ });
149
+ });
150
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Utilidad para obtener la ruta al módulo Python empaquetado.
4
+ * Funciona tanto en desarrollo como en ejecutables empaquetados con pkg.
5
+ */
6
+
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import fs from 'fs-extra';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ /**
14
+ * Retorna la ruta al directorio src/g360_core del módulo Python.
15
+ */
16
+ export function getPythonModulePath() {
17
+ // Ruta de desarrollo: py/src/g360_core relativo a este archivo
18
+ const devPath = path.join(__dirname, '..', 'py', 'src');
19
+ if (fs.existsSync(devPath)) {
20
+ return devPath;
21
+ }
22
+
23
+ // En pkg (ejecutable empaquetado)
24
+ const basePath = process.execPath || process.argv[0];
25
+ const pkgPath = path.join(basePath, '..', 'resources', 'py', 'src');
26
+ if (fs.existsSync(pkgPath)) {
27
+ return pkgPath;
28
+ }
29
+
30
+ // Fallback: directorio actual
31
+ const cwdPath = path.join(process.cwd(), 'py', 'src');
32
+ if (fs.existsSync(cwdPath)) {
33
+ return cwdPath;
34
+ }
35
+
36
+ return devPath;
37
+ }
38
+
39
+ /**
40
+ * Genera código Python que configura sys.path correctamente.
41
+ */
42
+ export function wrapPythonCode(pyCode) {
43
+ const modulePath = getPythonModulePath().replace(/\\/g, '\\\\');
44
+
45
+ return `
46
+ import sys
47
+ import os
48
+ from pathlib import Path
49
+
50
+ module_path = r"${modulePath}"
51
+ if module_path not in sys.path:
52
+ sys.path.insert(0, module_path)
53
+
54
+ ${pyCode}
55
+ `;
56
+ }
57
+
58
+ /**
59
+ * Ejecuta código Python y retorna su salida.
60
+ */
61
+ export async function runPython(pyCode) {
62
+ const fullCode = wrapPythonCode(pyCode);
63
+ const pyExec = process.env.PYTHON || 'python3';
64
+
65
+ const { spawn } = await import('child_process');
66
+ const proc = spawn(pyExec, ['-c', fullCode], {
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
69
+ });
70
+
71
+ let stdout = '';
72
+ let stderr = '';
73
+
74
+ await new Promise((resolve, reject) => {
75
+ proc.stdout?.on('data', (data) => { stdout += data.toString(); });
76
+ proc.stderr?.on('data', (data) => { stderr += data.toString(); });
77
+
78
+ proc.on('close', (code) => {
79
+ if (code === 0) resolve();
80
+ else reject(new Error(stderr || `Python código ${code}`));
81
+ });
82
+
83
+ proc.on('error', (err) => {
84
+ reject(new Error(`Python no disponible: ${err.message}`));
85
+ });
86
+ });
87
+
88
+ return { stdout, stderr };
89
+ }
@@ -1,3 +0,0 @@
1
- from g360_core.flet.ingestion_panel import IngestionPanel
2
-
3
- __all__ = ["IngestionPanel"]