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
package/src/commands/ingest.js
CHANGED
|
@@ -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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
const invalid
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -40,10 +40,21 @@ export async function init(name, options) {
|
|
|
40
40
|
|
|
41
41
|
const targetDir = path.join(process.cwd(), dir, name);
|
|
42
42
|
|
|
43
|
+
// Leer version del CLI desde package.json
|
|
44
|
+
const cliPkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
45
|
+
let cliVersion = '1.0.0';
|
|
46
|
+
try {
|
|
47
|
+
const cliPkg = fs.readJsonSync(cliPkgPath);
|
|
48
|
+
cliVersion = cliPkg.version || '1.0.0';
|
|
49
|
+
} catch {
|
|
50
|
+
// fallback si no se puede leer
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
console.log(chalk.bold.cyan('\n🚀 G360 Project Initialization\n'));
|
|
44
54
|
console.log(`Project: ${chalk.yellow(name)}`);
|
|
45
55
|
console.log(`Template: ${chalk.blue(template)}`);
|
|
46
56
|
console.log(`Skill: ${chalk.magenta(skill)}`);
|
|
57
|
+
console.log(`CLI Version: ${chalk.gray(cliVersion)}`);
|
|
47
58
|
console.log(`Target: ${chalk.gray(targetDir)}\n`);
|
|
48
59
|
|
|
49
60
|
let wantPortable = false;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file init.test.js
|
|
3
|
+
* @description Tests para el comando init
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
|
|
8
|
+
describe('init command', () => {
|
|
9
|
+
it('should export init function', async () => {
|
|
10
|
+
const mod = await import('../commands/init.js');
|
|
11
|
+
expect(mod).toHaveProperty('init');
|
|
12
|
+
expect(typeof mod.init).toBe('function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should accept name and options parameters', async () => {
|
|
16
|
+
const { init } = await import('../commands/init.js');
|
|
17
|
+
expect(init.length).toBe(2);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file list.test.js
|
|
3
|
+
* @description Tests para el comando list
|
|
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 { list } from '../commands/list.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
|
+
readdirSync: vi.fn(),
|
|
19
|
+
readJsonSync: vi.fn()
|
|
20
|
+
}
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('list command', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('list templates', () => {
|
|
29
|
+
it('should list available templates', async () => {
|
|
30
|
+
const mockTemplates = ['web-pwa', 'svelte-web', 'solid-web'];
|
|
31
|
+
fs.existsSync.mockReturnValue(true);
|
|
32
|
+
fs.readdirSync.mockReturnValue(mockTemplates);
|
|
33
|
+
|
|
34
|
+
await list('templates', { json: false });
|
|
35
|
+
|
|
36
|
+
expect(fs.readdirSync).toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return empty array when templates directory does not exist', async () => {
|
|
40
|
+
fs.existsSync.mockReturnValue(false);
|
|
41
|
+
|
|
42
|
+
await list('templates', { json: false });
|
|
43
|
+
|
|
44
|
+
expect(fs.readdirSync).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('list skills', () => {
|
|
49
|
+
it('should list skills from g360-skills.json', async () => {
|
|
50
|
+
const mockSkills = {
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' },
|
|
53
|
+
{ name: 'moderno', description: 'Herramientas modernas', device: 'movil' }
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
fs.existsSync.mockReturnValue(true);
|
|
58
|
+
fs.readJsonSync.mockReturnValue(mockSkills);
|
|
59
|
+
|
|
60
|
+
await list('skills', { json: false });
|
|
61
|
+
|
|
62
|
+
expect(fs.readJsonSync).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle missing g360-skills.json gracefully', async () => {
|
|
66
|
+
fs.existsSync.mockReturnValue(false);
|
|
67
|
+
|
|
68
|
+
await list('skills', { json: false });
|
|
69
|
+
|
|
70
|
+
expect(fs.readJsonSync).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('list snippets', () => {
|
|
75
|
+
it('should list snippets from snippets.json', async () => {
|
|
76
|
+
const mockSnippets = {
|
|
77
|
+
snippets: [
|
|
78
|
+
{ name: 'cli-argparse-basic', description: 'Basic argparse CLI', language: 'python' },
|
|
79
|
+
{ name: 'g360-button', description: 'G360 styled button', language: 'html' }
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
fs.existsSync.mockReturnValue(true);
|
|
84
|
+
fs.readJsonSync.mockReturnValue(mockSnippets);
|
|
85
|
+
|
|
86
|
+
await list('snippets', { json: false });
|
|
87
|
+
|
|
88
|
+
expect(fs.readJsonSync).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle missing snippets.json gracefully', async () => {
|
|
92
|
+
fs.existsSync.mockReturnValue(false);
|
|
93
|
+
|
|
94
|
+
await list('snippets', { json: false });
|
|
95
|
+
|
|
96
|
+
expect(fs.readJsonSync).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('list all', () => {
|
|
101
|
+
it('should list all asset types', async () => {
|
|
102
|
+
const mockTemplates = ['web-pwa', 'svelte-web'];
|
|
103
|
+
const mockSkills = {
|
|
104
|
+
skills: [{ name: 'corporativo', description: 'Proyectos corporativos', device: 'pc' }]
|
|
105
|
+
};
|
|
106
|
+
const mockSnippets = {
|
|
107
|
+
snippets: [{ name: 'cli-argparse-basic', description: 'Basic argparse', language: 'python' }]
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
fs.existsSync.mockReturnValue(true);
|
|
111
|
+
fs.readdirSync.mockReturnValue(mockTemplates);
|
|
112
|
+
fs.readJsonSync.mockReturnValue(mockSkills).mockReturnValueOnce(mockSnippets);
|
|
113
|
+
|
|
114
|
+
await list('all', { json: false });
|
|
115
|
+
|
|
116
|
+
expect(fs.readdirSync).toHaveBeenCalled();
|
|
117
|
+
expect(fs.readJsonSync).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('JSON output', () => {
|
|
122
|
+
it('should output JSON when requested', async () => {
|
|
123
|
+
const mockTemplates = ['web-pwa'];
|
|
124
|
+
fs.existsSync.mockReturnValue(true);
|
|
125
|
+
fs.readdirSync.mockReturnValue(mockTemplates);
|
|
126
|
+
|
|
127
|
+
await list('templates', { json: true });
|
|
128
|
+
|
|
129
|
+
expect(fs.readdirSync).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
package/src/commands/scan.js
CHANGED
|
@@ -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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
invalid
|
|
33
|
-
|
|
34
|
-
print(
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log(chalk.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
@@ -12,11 +12,13 @@ import { fileURLToPath } from 'url';
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const SKILLS_PATH = path.resolve(__dirname, '../assets/config/g360-skills.json');
|
|
14
14
|
|
|
15
|
-
export async function setSkill(skillName, options) {
|
|
16
|
-
const { verbose = false, cwd = process.cwd() } = options;
|
|
15
|
+
export async function setSkill(skillName, options = {}) {
|
|
16
|
+
const { verbose = false, cwd = process.cwd(), force = false } = options;
|
|
17
17
|
const isInternalCall = options.cwd !== undefined;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
if (!isInternalCall) {
|
|
20
|
+
console.log(chalk.bold.cyan('\n🎨 G360 Skill Selector\n'));
|
|
21
|
+
}
|
|
20
22
|
|
|
21
23
|
// Cargar skills disponibles
|
|
22
24
|
let skillsConfig;
|
|
@@ -43,9 +45,9 @@ export async function setSkill(skillName, options) {
|
|
|
43
45
|
const skillJsonPath = path.join(cwd, 'skill.json');
|
|
44
46
|
|
|
45
47
|
if (fs.existsSync(skillJsonPath)) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (!force) {
|
|
49
|
+
console.log(chalk.yellow('⚠️ El proyecto ya tiene un skill configurado.'));
|
|
50
|
+
console.log(chalk.gray('Usar --force para sobrescribir'));
|
|
49
51
|
console.log(chalk.cyan('\nPara cambiar el skill:'));
|
|
50
52
|
console.log(chalk.cyan(' g360 set-skill ') + skillName + chalk.cyan(' --force'));
|
|
51
53
|
return;
|
|
@@ -65,17 +67,19 @@ export async function setSkill(skillName, options) {
|
|
|
65
67
|
|
|
66
68
|
await fs.writeJson(skillJsonPath, skillData, { spaces: 2 });
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
if (!isInternalCall) {
|
|
71
|
+
console.log(chalk.green(`\n✅ Skill "${skillName}" configurado correctamente`));
|
|
72
|
+
console.log(chalk.gray('\nDetalles:'));
|
|
73
|
+
console.log(` Device: ${skill.device}`);
|
|
74
|
+
console.log(` Accent: ${skill.colors.accent}`);
|
|
75
|
+
console.log(` Signature: ${skill.signature.mode}`);
|
|
76
|
+
|
|
77
|
+
if (verbose) {
|
|
78
|
+
console.log(chalk.gray('\nColores:'));
|
|
79
|
+
Object.entries(skill.colors).forEach(([key, value]) => {
|
|
80
|
+
console.log(` ${key}: ${value}`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
} catch (error) {
|