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 +47 -6
- package/package.json +1 -1
- package/src/cli.js +10 -0
- package/src/commands/addon.js +188 -0
- package/src/commands/ingest.js +78 -84
- package/src/commands/scan.js +68 -80
- package/src/commands/validate.js +90 -66
- package/src/lib/python_runner.js +8 -8
- 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/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 (
|
|
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 —
|
|
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.
|
|
872
|
-
- `commands/`: init, bring, list, audit, set-skill
|
|
873
|
-
- `lib/`: manifest, validator, asset-validator
|
|
874
|
-
- **
|
|
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
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
|
+
}
|
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/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
|
+
}
|