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.
- package/README.md +83 -8
- package/package.json +16 -6
- package/py/pyproject.toml +4 -4
- package/py/requirements.txt +4 -0
- package/py/src/g360_core/__init__.py +67 -4
- package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
- package/py/src/g360_core/batch_processor.py +120 -0
- package/py/src/g360_core/commercial_engine.py +305 -0
- package/py/src/g360_core/logger.py +40 -0
- package/py/src/g360_core/pipeline.py +578 -0
- package/py/src/g360_core/processor.py +634 -0
- package/py/src/g360_core/processor_segmentacion.py +859 -0
- package/py/src/g360_core/processor_sku.py +427 -0
- package/py/src/g360_core/scanner.py +218 -0
- package/py/src/g360_core/utils.py +435 -0
- package/src/cli.js +35 -2
- package/src/commands/addon.js +188 -0
- package/src/commands/ingest.js +187 -0
- package/src/commands/scan.js +90 -0
- package/src/commands/validate.js +150 -0
- package/src/lib/python_runner.js +89 -0
- package/py/src/g360_core/flet/__init__.py +0 -3
- package/py/src/g360_core/flet/ingestion_panel.py +0 -218
- package/py/src/g360_core/ingestion.py +0 -480
- package/src/assets/engine/g360-data-validator.js +0 -44
- package/src/assets/engine/g360-engine.js +0 -12
- package/src/assets/engine/g360-field-mapper.js +0 -35
- package/src/assets/engine/g360-skill-audit.mjs +0 -37
- package/src/assets/engine/g360-skill-meta-evaluator.mjs +0 -33
- package/src/lib/assets.js +0 -38
- package/src/lib/checksum.js +0 -27
- package/src/lib/config.js +0 -23
- package/src/lib/offline.js +0 -33
- package/src/lib/presenter.js +0 -24
- package/src/lib/rollback.js +0 -49
- 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
|
+
}
|