g360-cli 1.9.0 → 1.10.1
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 +65 -7
- package/package.json +11 -2
- package/src/cli.js +54 -0
- package/src/commands/addon.js +188 -0
- package/src/commands/addon.test.js +37 -0
- package/src/commands/audit.test.js +32 -0
- package/src/commands/bring.test.js +37 -0
- package/src/commands/ingest.js +78 -84
- package/src/commands/init.js +11 -0
- package/src/commands/init.test.js +19 -0
- package/src/commands/list.test.js +132 -0
- package/src/commands/scan.js +68 -80
- package/src/commands/set-skill.js +21 -17
- package/src/commands/set-skill.test.js +134 -0
- package/src/commands/signature.js +46 -26
- package/src/commands/update.js +14 -2
- package/src/commands/validate.js +90 -66
- package/src/lib/asset-validator.test.js +237 -0
- package/src/lib/manifest.test.js +173 -0
- package/src/lib/python_runner.js +8 -8
- package/src/lib/validator.test.js +115 -0
- 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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file set-skill.test.js
|
|
3
|
+
* @description Tests para el comando set-skill
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { setSkill } from '../commands/set-skill.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Mock de fs-extra
|
|
15
|
+
vi.mock('fs-extra', () => ({
|
|
16
|
+
default: {
|
|
17
|
+
existsSync: vi.fn(),
|
|
18
|
+
readJson: vi.fn(),
|
|
19
|
+
readJsonSync: vi.fn(),
|
|
20
|
+
writeJson: vi.fn()
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('set-skill command', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
// Mockear console.log para evitar output en los tests
|
|
28
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
29
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('valid skill selection', () => {
|
|
37
|
+
it('should set skill successfully', async () => {
|
|
38
|
+
const mockSkills = {
|
|
39
|
+
skills: [
|
|
40
|
+
{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fs.readJson.mockResolvedValue(mockSkills);
|
|
45
|
+
fs.existsSync.mockReturnValue(false);
|
|
46
|
+
fs.writeJson.mockResolvedValue();
|
|
47
|
+
|
|
48
|
+
await setSkill('corporativo', { verbose: false, cwd: process.cwd() });
|
|
49
|
+
|
|
50
|
+
expect(fs.writeJson).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should overwrite existing skill with --force', async () => {
|
|
54
|
+
const mockSkills = {
|
|
55
|
+
skills: [
|
|
56
|
+
{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
fs.readJson.mockResolvedValue(mockSkills);
|
|
61
|
+
fs.existsSync.mockReturnValue(true);
|
|
62
|
+
fs.writeJson.mockResolvedValue();
|
|
63
|
+
|
|
64
|
+
await setSkill('corporativo', { verbose: false, force: true, cwd: process.cwd() });
|
|
65
|
+
|
|
66
|
+
expect(fs.writeJson).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('invalid skill selection', () => {
|
|
71
|
+
it('should reject invalid skill name', async () => {
|
|
72
|
+
const mockSkills = {
|
|
73
|
+
skills: [
|
|
74
|
+
{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' }
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
fs.readJson.mockResolvedValue(mockSkills);
|
|
79
|
+
|
|
80
|
+
await setSkill('invalid-skill', { verbose: false, cwd: process.cwd() });
|
|
81
|
+
|
|
82
|
+
expect(fs.writeJson).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle missing skills config file', async () => {
|
|
86
|
+
fs.readJson.mockRejectedValue(new Error('File not found'));
|
|
87
|
+
|
|
88
|
+
await setSkill('corporativo', { verbose: false, cwd: process.cwd() });
|
|
89
|
+
|
|
90
|
+
expect(fs.writeJson).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('skill already exists', () => {
|
|
95
|
+
it('should not overwrite without --force', async () => {
|
|
96
|
+
const mockSkills = {
|
|
97
|
+
skills: [
|
|
98
|
+
{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc', colors: { accent: '#00796B' }, signature: { mode: 'powered' } }
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
fs.readJsonSync.mockReturnValue(mockSkills);
|
|
103
|
+
fs.existsSync.mockReturnValue(true);
|
|
104
|
+
|
|
105
|
+
await setSkill('corporativo', { verbose: false, force: false, cwd: process.cwd() });
|
|
106
|
+
|
|
107
|
+
expect(fs.writeJson).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('verbose output', () => {
|
|
112
|
+
it('should show detailed colors with --verbose', async () => {
|
|
113
|
+
const mockSkills = {
|
|
114
|
+
skills: [
|
|
115
|
+
{
|
|
116
|
+
name: 'corporativo',
|
|
117
|
+
description: 'Proyectos corporativos',
|
|
118
|
+
device: 'pc',
|
|
119
|
+
colors: { bg: '#0b1220', surface: '#151e2e', accent: '#00796B', text: '#f0f4f8', muted: '#94a3b8' },
|
|
120
|
+
signature: { mode: 'powered' }
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
fs.readJson.mockResolvedValue(mockSkills);
|
|
126
|
+
fs.existsSync.mockReturnValue(false);
|
|
127
|
+
fs.writeJson.mockResolvedValue();
|
|
128
|
+
|
|
129
|
+
await setSkill('corporativo', { verbose: true, cwd: process.cwd() });
|
|
130
|
+
|
|
131
|
+
expect(fs.writeJson).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const SIGNATURE_ASSETS = path.join(__dirname, '..', 'assets', 'signature');
|
|
8
9
|
|
|
10
|
+
const VALID_COMMANDS = ['install', 'positions'];
|
|
11
|
+
|
|
9
12
|
const POSITIONS = {
|
|
10
13
|
'bottom-right': 'position: fixed; bottom: 16px; right: 16px; z-index: 99999;',
|
|
11
14
|
'bottom-left': 'position: fixed; bottom: 16px; left: 16px; z-index: 99999;',
|
|
@@ -22,6 +25,18 @@ const FLET_POSITIONS = {
|
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
export async function signature(command, options) {
|
|
28
|
+
// Validacion del comando
|
|
29
|
+
if (!VALID_COMMANDS.includes(command)) {
|
|
30
|
+
console.error(chalk.red(`❌ Comando invalido: "${command}"`));
|
|
31
|
+
console.log(chalk.gray('Comandos disponibles:'));
|
|
32
|
+
console.log(chalk.gray(' install - Instalar g360-signature en un proyecto'));
|
|
33
|
+
console.log(chalk.gray(' positions - Mostrar posiciones disponibles'));
|
|
34
|
+
console.log(chalk.gray('\nEjemplo:'));
|
|
35
|
+
console.log(chalk.gray(' g360 signature install'));
|
|
36
|
+
console.log(chalk.gray(' g360 signature positions'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
const {
|
|
26
41
|
path: targetPath = '.',
|
|
27
42
|
force = false,
|
|
@@ -43,22 +58,45 @@ export async function signature(command, options) {
|
|
|
43
58
|
return;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
// Modo interactivo: guiar al usuario
|
|
61
|
+
// Modo interactivo: guiar al usuario con inquirer
|
|
62
|
+
let resolvedPosition = position;
|
|
63
|
+
let resolvedMode = mode;
|
|
64
|
+
|
|
47
65
|
if (interactive) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const answers = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'list',
|
|
69
|
+
name: 'mode',
|
|
70
|
+
message: 'Selecciona el modo de la firma:',
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: 'Powered by G360 (recomendado)', value: 'powered' },
|
|
73
|
+
{ name: 'Own (G360 by ccusi)', value: 'own' },
|
|
74
|
+
],
|
|
75
|
+
default: 'powered',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'list',
|
|
79
|
+
name: 'position',
|
|
80
|
+
message: 'Selecciona la posicion de la firma:',
|
|
81
|
+
choices: projectType === 'flet'
|
|
82
|
+
? Object.entries(FLET_POSITIONS).map(([key, desc]) => ({ name: `${key}: ${desc}`, value: key }))
|
|
83
|
+
: Object.entries(POSITIONS).map(([key, desc]) => ({ name: `${key}: ${desc}`, value: key })),
|
|
84
|
+
default: 'bottom-right',
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
resolvedMode = answers.mode;
|
|
89
|
+
resolvedPosition = answers.position;
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
if (projectType === 'flet') {
|
|
55
|
-
await installFlet(targetDir, { force, mode, version, position:
|
|
93
|
+
await installFlet(targetDir, { force, mode: resolvedMode, version, position: resolvedPosition });
|
|
56
94
|
} else if (projectType === 'web') {
|
|
57
|
-
await installWeb(targetDir, { force, mode, version, position:
|
|
95
|
+
await installWeb(targetDir, { force, mode: resolvedMode, version, position: resolvedPosition });
|
|
58
96
|
}
|
|
59
97
|
|
|
60
98
|
console.log(chalk.green('\n✅ g360-signature instalado exitosamente!'));
|
|
61
|
-
showUsageTips(projectType,
|
|
99
|
+
showUsageTips(projectType, resolvedPosition);
|
|
62
100
|
}
|
|
63
101
|
|
|
64
102
|
if (command === 'positions') {
|
|
@@ -96,24 +134,6 @@ function showPositions() {
|
|
|
96
134
|
console.log(chalk.gray('\nEjemplo: g360 signature install --position bottom-left\n'));
|
|
97
135
|
}
|
|
98
136
|
|
|
99
|
-
async function interactivePosition(projectType) {
|
|
100
|
-
console.log(chalk.bold.cyan('\n📍 Selecciona la posicion de la firma:\n'));
|
|
101
|
-
|
|
102
|
-
const options = projectType === 'flet'
|
|
103
|
-
? Object.entries(FLET_POSITIONS)
|
|
104
|
-
: Object.entries(POSITIONS);
|
|
105
|
-
|
|
106
|
-
options.forEach(([key, value], index) => {
|
|
107
|
-
console.log(chalk.white(` ${index + 1}. ${key.padEnd(18)} ${chalk.gray(value)}`));
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
console.log(chalk.gray('\n Presiona Enter para usar la posicion por defecto (bottom-right)'));
|
|
111
|
-
console.log(chalk.gray(' O escribe el nombre de la posicion\n'));
|
|
112
|
-
|
|
113
|
-
// En modo no-interactivo, retornar default
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
137
|
function showUsageTips(projectType, position) {
|
|
118
138
|
console.log(chalk.bold.cyan('\n💡 Tips de uso:\n'));
|
|
119
139
|
|
package/src/commands/update.js
CHANGED
|
@@ -19,10 +19,22 @@ export async function update(options) {
|
|
|
19
19
|
if (check) {
|
|
20
20
|
try {
|
|
21
21
|
console.log(chalk.gray(`Current version: ${currentVersion}`));
|
|
22
|
-
console.log(chalk.gray('
|
|
23
|
-
|
|
22
|
+
console.log(chalk.gray('Checking npm for latest version...\n'));
|
|
23
|
+
|
|
24
|
+
const latestVersion = execSync('npm view g360-cli version', { encoding: 'utf8' }).trim();
|
|
25
|
+
|
|
26
|
+
if (latestVersion === currentVersion) {
|
|
27
|
+
console.log(chalk.green(`✅ You have the latest version (${currentVersion})`));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.yellow(`📦 Latest version: ${latestVersion}`));
|
|
30
|
+
console.log(chalk.yellow(`📦 Current version: ${currentVersion}`));
|
|
31
|
+
console.log(chalk.cyan('\nTo update:'));
|
|
32
|
+
console.log(chalk.gray(' npm install -g g360-cli@latest'));
|
|
33
|
+
}
|
|
24
34
|
} catch (error) {
|
|
25
35
|
console.error(chalk.red(`Error checking version: ${error.message}`));
|
|
36
|
+
console.log(chalk.gray('\nTry manually:'));
|
|
37
|
+
console.log(chalk.gray(' npm view g360-cli version'));
|
|
26
38
|
}
|
|
27
39
|
return;
|
|
28
40
|
}
|
package/src/commands/validate.js
CHANGED
|
@@ -4,88 +4,86 @@
|
|
|
4
4
|
* Valida archivos ERP sin procesar completamente.
|
|
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
|
import fs from 'fs-extra';
|
|
12
12
|
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for (const subEntry of sub) {
|
|
44
|
-
if (subEntry.isFile() && extPattern.test(subEntry.name)) {
|
|
45
|
-
filesToCheck.push(path.join(fullPath, subEntry.name));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
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));
|
|
48
43
|
}
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.warn(chalk.yellow(` ⚠ No se pudo acceder a ${p}: ${err.message}`));
|
|
53
46
|
}
|
|
54
47
|
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn(chalk.yellow(` ⚠ No se pudo acceder a ${p}: ${err.message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
if (filesToCheck.length === 0) {
|
|
54
|
+
console.warn(chalk.yellow(' No se encontraron archivos para validar'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
const results = [];
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
80
|
+
console.log(chalk.gray(`\n📊 Resumen: ${validCount}/${results.length} archivos válidos`));
|
|
81
|
+
}
|
|
86
82
|
|
|
87
|
-
async function validateSingleFile(filepath
|
|
83
|
+
async function validateSingleFile(filepath) {
|
|
88
84
|
const pyCode = `
|
|
85
|
+
import sys
|
|
86
|
+
sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
|
|
89
87
|
from g360_core.scanner import ERPScanner
|
|
90
88
|
from pathlib import Path
|
|
91
89
|
|
|
@@ -102,12 +100,11 @@ except Exception as e:
|
|
|
102
100
|
`;
|
|
103
101
|
|
|
104
102
|
try {
|
|
105
|
-
const { runPython } = await import('../lib/python_runner.js');
|
|
106
103
|
const result = await runPython(pyCode);
|
|
107
104
|
const lines = result.stdout.split('\n').filter(l => l.trim());
|
|
108
105
|
|
|
109
106
|
let valid = false;
|
|
110
|
-
let missing
|
|
107
|
+
let missing = [];
|
|
111
108
|
|
|
112
109
|
for (const line of lines) {
|
|
113
110
|
if (line.startsWith('VALID=')) {
|
|
@@ -120,7 +117,34 @@ except Exception as e:
|
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
return { path: filepath, valid, missing };
|
|
123
|
-
} catch (err
|
|
120
|
+
} catch (err) {
|
|
124
121
|
return { path: filepath, valid: false, missing: [err.message] };
|
|
125
122
|
}
|
|
126
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
|
+
}
|