g360-cli 1.13.1 → 1.15.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 (77) hide show
  1. package/package.json +71 -71
  2. package/src/assets/config/g360-skills.json +165 -0
  3. package/src/assets/config/project-types.json +23 -4
  4. package/src/assets/snippets/snippets.json +4 -0
  5. package/src/assets/templates/lit-web/src/index.js +1 -1
  6. package/src/assets/templates/python-cli/src/core/__init__.py +0 -0
  7. package/src/assets/templates/python-flet/README.md +6 -1
  8. package/src/assets/templates/python-flet/pyproject.toml +2 -2
  9. package/src/assets/templates/python-flet/src/core/skill.json +1 -1
  10. package/src/assets/templates/python-flet-migrate/pyproject.toml +1 -3
  11. package/src/assets/templates/python-flet-polished/README.md +170 -0
  12. package/src/assets/templates/python-flet-polished/assets/data/sample_data.json +12 -0
  13. package/src/assets/templates/python-flet-polished/assets/fonts/Inter-Variable.ttf +0 -0
  14. package/src/assets/templates/python-flet-polished/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
  15. package/src/assets/templates/python-flet-polished/assets/images/app.ico +0 -0
  16. package/src/assets/templates/python-flet-polished/assets/images/logo.svg +9 -0
  17. package/src/assets/templates/python-flet-polished/build-portable.bat +42 -0
  18. package/src/assets/templates/python-flet-polished/create_shortcut.vbs +15 -0
  19. package/src/assets/templates/python-flet-polished/g360_flet/__init__.py +0 -0
  20. package/src/assets/templates/python-flet-polished/g360_flet/g360_signature.py +210 -0
  21. package/src/assets/templates/python-flet-polished/launch.vbs +3 -0
  22. package/src/assets/templates/python-flet-polished/launch_minimized.bat +3 -0
  23. package/src/assets/templates/python-flet-polished/main.py +47 -0
  24. package/src/assets/templates/python-flet-polished/pyproject.toml +18 -0
  25. package/src/assets/templates/python-flet-polished/requirements.txt +3 -0
  26. package/src/assets/templates/python-flet-polished/run.bat +189 -0
  27. package/src/assets/templates/python-flet-polished/skill.json +28 -0
  28. package/src/assets/templates/python-flet-polished/src/__init__.py +0 -0
  29. package/src/assets/templates/python-flet-polished/src/app.py +343 -0
  30. package/src/assets/templates/python-flet-polished/src/config/__init__.py +0 -0
  31. package/src/assets/templates/python-flet-polished/src/config/theme.py +122 -0
  32. package/src/assets/templates/python-flet-polished/src/core/__init__.py +0 -0
  33. package/src/assets/templates/python-flet-polished/src/core/constants.py +49 -0
  34. package/src/assets/templates/python-flet-polished/src/core/processor.py +128 -0
  35. package/src/assets/templates/python-flet-polished/src/core/skill.json +40 -0
  36. package/src/assets/templates/python-flet-polished/src/ui/__init__.py +0 -0
  37. package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +308 -0
  38. package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +69 -0
  39. package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +143 -0
  40. package/src/assets/templates/python-flet-polished/sync_portable.py +71 -0
  41. package/src/assets/templates/solid-web/src/components/App.jsx +2 -2
  42. package/src/cli.js +4 -4
  43. package/src/commands/clean.js +0 -12
  44. package/src/commands/docs.js +105 -1
  45. package/src/commands/ingest.js +11 -61
  46. package/src/commands/init.js +34 -3
  47. package/src/commands/lint.js +35 -77
  48. package/src/commands/scan.js +9 -37
  49. package/src/commands/validate.js +6 -36
  50. package/src/lib/file-utils.js +89 -2
  51. package/src/lib/python-runner.js +77 -0
  52. package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
  53. package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
  54. package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
  55. package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
  56. package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
  57. package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
  58. package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
  59. package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
  60. package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
  61. package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
  62. package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
  63. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
  64. package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
  65. package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
  66. package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
  67. package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
  68. package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
  69. package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
  70. package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
  71. package/src/assets/ingestion/core/__pycache__/ingestion.cpython-312.pyc +0 -0
  72. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-312.pyc +0 -0
  73. package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-312.pyc +0 -0
  75. package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-314.pyc +0 -0
  76. package/src/assets/templates/python-cli/package.json +0 -7
  77. /package/src/assets/config/{skills.json → agent-skills.json} +0 -0
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const BRAND_PATH = path.join(__dirname, '..', '..', 'assets', 'brand', 'brand.json');
7
+ const BRAND_PATH = path.join(__dirname, '..', 'assets', 'brand', 'brand.json');
8
8
 
9
9
  const LEVELS = ['readme', 'architecture', 'business-rules', 'api', 'dependencies', 'classes', 'code-graph', 'all'];
10
10
 
@@ -176,6 +176,7 @@ async function generateLevel(lvl, dir, projectInfo, brand, manifest, skill, dryR
176
176
  case 'dependencies': return await generateDependencies(dir, projectInfo, dryRun);
177
177
  case 'classes': return await generateClasses(dir, projectInfo, dryRun);
178
178
  case 'code-graph': return await generateCodeGraph(dir, projectInfo, dryRun);
179
+ case 'api': return await generateApi(dir, projectInfo, dryRun);
179
180
  default: return null;
180
181
  }
181
182
  }
@@ -537,6 +538,109 @@ async function generateCodeGraph(dir, projectInfo, dryRun) {
537
538
  return 'docs/generated/code_graph.mmd';
538
539
  }
539
540
 
541
+ async function generateApi(dir, projectInfo, dryRun) {
542
+ const scan = scanProject(dir);
543
+ const pyFiles = scan.pyFiles.filter(f => !f.endsWith('__init__.py'));
544
+ const jsFiles = scan.jsFiles.filter(f => !f.endsWith('.test.js') && !f.endsWith('.spec.js'));
545
+
546
+ const exportedFunctions = [];
547
+ const exportedClasses = [];
548
+
549
+ // Python: detectar def y class exportados
550
+ for (const file of pyFiles) {
551
+ const filePath = path.join(dir, file);
552
+ try {
553
+ const content = fs.readFileSync(filePath, 'utf8');
554
+ const funcMatches = content.match(/^(?:def|async\s+def)\s+(\w+)\s*\(/gm);
555
+ if (funcMatches) {
556
+ for (const m of funcMatches) {
557
+ const name = m.replace(/^(?:async\s+)?def\s+/, '').replace(/\s*\(.*/, '');
558
+ if (!name.startsWith('_')) {
559
+ exportedFunctions.push({ file, name, type: 'python' });
560
+ }
561
+ }
562
+ }
563
+ const classMatches = content.match(/^(?:class)\s+(\w+)/gm);
564
+ if (classMatches) {
565
+ for (const m of classMatches) {
566
+ const name = m.replace(/class\s+/, '').replace(/\s*[:(\{].*/, '');
567
+ if (!name.startsWith('_')) {
568
+ exportedClasses.push({ file, name, type: 'python' });
569
+ }
570
+ }
571
+ }
572
+ } catch { /* skip unreadable */ }
573
+ }
574
+
575
+ // JS/TS: detectar export function y export class
576
+ for (const file of jsFiles) {
577
+ const filePath = path.join(dir, file);
578
+ try {
579
+ const content = fs.readFileSync(filePath, 'utf8');
580
+ const funcMatches = content.match(/^export\s+(?:async\s+)?function\s+(\w+)/gm);
581
+ if (funcMatches) {
582
+ for (const m of funcMatches) {
583
+ const name = m.replace(/^export\s+(?:async\s+)?function\s+/, '');
584
+ exportedFunctions.push({ file, name, type: 'javascript' });
585
+ }
586
+ }
587
+ const classMatches = content.match(/^export\s+class\s+(\w+)/gm);
588
+ if (classMatches) {
589
+ for (const m of classMatches) {
590
+ const name = m.replace(/^export\s+class\s+/, '');
591
+ exportedClasses.push({ file, name, type: 'javascript' });
592
+ }
593
+ }
594
+ } catch { /* skip unreadable */ }
595
+ }
596
+
597
+ if (exportedFunctions.length === 0 && exportedClasses.length === 0) {
598
+ console.log(chalk.gray(' ⚠ No se encontraron exports publicos. Generando API basica.'));
599
+ }
600
+
601
+ // Generar markdown
602
+ let md = `# API Reference\n\n`;
603
+ md += `> Generado automaticamente por \`g360 docs api\`\n\n`;
604
+
605
+ if (exportedClasses.length > 0) {
606
+ md += `## Clases\n\n`;
607
+ md += `| Clase | Archivo | Tipo |\n`;
608
+ md += `|-------|---------|------|\n`;
609
+ for (const c of exportedClasses) {
610
+ md += `| \`${c.name}\` | \`${c.file}\` | ${c.type} |\n`;
611
+ }
612
+ md += `\n`;
613
+ }
614
+
615
+ if (exportedFunctions.length > 0) {
616
+ md += `## Funciones\n\n`;
617
+ md += `| Funcion | Archivo | Tipo |\n`;
618
+ md += `|---------|---------|------|\n`;
619
+ for (const f of exportedFunctions) {
620
+ md += `| \`${f.name}\` | \`${f.file}\` | ${f.type} |\n`;
621
+ }
622
+ md += `\n`;
623
+ }
624
+
625
+ if (exportedFunctions.length === 0 && exportedClasses.length === 0) {
626
+ md += `_No se encontraron exports publicos en el proyecto._\n`;
627
+ }
628
+
629
+ const outputDir = path.join(dir, 'docs', 'generated');
630
+ const outputPath = path.join(outputDir, 'api.md');
631
+
632
+ if (dryRun) {
633
+ console.log(chalk.gray(` [dry-run] docs/generated/api.md`));
634
+ console.log(chalk.gray(` Funciones: ${exportedFunctions.length} | Clases: ${exportedClasses.length}`));
635
+ return null;
636
+ }
637
+
638
+ await fs.ensureDir(outputDir);
639
+ await fs.writeFile(outputPath, md, 'utf8');
640
+ console.log(chalk.green(` ✅ docs/generated/api.md (${exportedFunctions.length} funciones, ${exportedClasses.length} clases)`));
641
+ return 'docs/generated/api.md';
642
+ }
643
+
540
644
  function scanProject(dir) {
541
645
  const results = {
542
646
  files: [],
@@ -7,7 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { spawn } from 'child_process';
10
+ import { runPython, runPythonStdout, g360CorePath } from '../lib/python-runner.js';
11
11
  import fs from 'fs-extra';
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -38,12 +38,12 @@ export async function ingest(input, options) {
38
38
  const result = await scanDirectory(inputPath);
39
39
  filepaths = result.valid.map(info => info.path);
40
40
  if (filepaths.length === 0) {
41
- console.error(chalk.red('❌ No se encontraron archivos ERP válidos en el directorio'));
41
+ console.error(chalk.red('❌ No se encontraron archivos ERP validos en el directorio'));
42
42
  process.exit(1);
43
43
  }
44
- console.log(chalk.green(` Encontrados ${filepaths.length} archivos válidos`));
44
+ console.log(chalk.green(` Encontrados ${filepaths.length} archivos validos`));
45
45
  } else {
46
- console.error(chalk.red(`❌ Ruta no válida: ${inputPath}`));
46
+ console.error(chalk.red(`❌ Ruta no valida: ${inputPath}`));
47
47
  process.exit(1);
48
48
  }
49
49
 
@@ -90,8 +90,7 @@ export async function ingest(input, options) {
90
90
 
91
91
  async function scanDirectory(dir) {
92
92
  const pyCode = `
93
- import sys
94
- sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
93
+ ${g360CorePath(__dirname)}
95
94
  from g360_core.scanner import find_erp_files_in_dir
96
95
  from pathlib import Path
97
96
 
@@ -102,8 +101,8 @@ for i in invalid[:10]:
102
101
  print(f"INVALID::{i.path.name}::${i.error_msg}")
103
102
  `;
104
103
 
105
- const result = await runPython(pyCode);
106
- const lines = result.stdout.split('\n').filter(l => l.trim());
104
+ const { stdout } = await runPython(pyCode);
105
+ const lines = stdout.split('\n').filter(l => l.trim());
107
106
  const valid = [];
108
107
  const invalid = [];
109
108
 
@@ -122,66 +121,17 @@ for i in invalid[:10]:
122
121
 
123
122
  async function runBatchIngest(filepaths) {
124
123
  const pyCode = `
125
- import sys
126
- sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
124
+ ${g360CorePath(__dirname)}
127
125
  from g360_core.scanner import batch_process_files
128
126
  from pathlib import Path
129
127
  import pandas as pd
128
+ import sys
130
129
 
131
130
  filepaths = [${JSON.stringify(filepaths).replace(/"/g, "'")}]
132
131
  df = batch_process_files([Path(p) for p in filepaths], merge_results=True)
133
132
  sys.stdout.write(df.to_csv(index=False))
134
133
  `;
135
134
 
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
- });
135
+ const { stdout } = await runPython(pyCode);
136
+ return stdout;
160
137
  }
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
- }
@@ -10,7 +10,24 @@ import inquirer from 'inquirer';
10
10
 
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
 
13
- const PORTABLE_TEMPLATES = ['python-flet', 'python-flet-migrate', 'python-cli', 'python-customtkinter'];
13
+ const PORTABLE_TEMPLATES = ['python-flet', 'python-flet-migrate', 'python-flet-polished', 'python-cli', 'python-customtkinter'];
14
+
15
+ // Templates por defecto segun skill (mapeo automatico)
16
+ const TEMPLATE_DEFAULTS = {
17
+ 'flet-desktop': 'python-flet-polished',
18
+ 'flet-desktop-corporativo': 'python-flet-polished',
19
+ 'flet-desktop-polished': 'python-flet-polished',
20
+ 'cipsa': 'python-flet-polished',
21
+ 'cipsa-movil': 'python-flet-polished',
22
+ 'corporativo': 'web-pwa',
23
+ 'corporativo-movil': 'web-pwa',
24
+ 'corporativo-g360': 'web-pwa',
25
+ 'corporativo-g360-movil': 'web-pwa',
26
+ 'moderno': 'web-pwa',
27
+ 'moderno-movil': 'web-pwa',
28
+ 'minimalista': 'python-cli',
29
+ 'custom': 'web-pwa',
30
+ };
14
31
 
15
32
  async function askPortableOption(template) {
16
33
  if (!PORTABLE_TEMPLATES.includes(template)) {
@@ -31,7 +48,7 @@ async function askPortableOption(template) {
31
48
 
32
49
  export async function init(name, options) {
33
50
  const {
34
- template = 'web-pwa',
51
+ template: rawTemplate,
35
52
  skill = 'corporativo-movil',
36
53
  dir = '.',
37
54
  dryRun = false,
@@ -40,6 +57,20 @@ export async function init(name, options) {
40
57
  brand = false,
41
58
  } = options;
42
59
 
60
+ // Resolver template: usar default segun skill si no se especifico
61
+ let template = rawTemplate || TEMPLATE_DEFAULTS[skill] || 'web-pwa';
62
+
63
+ // Advertir si se usa template legacy
64
+ if (template === 'python-flet') {
65
+ console.log(chalk.yellow('⚠️ Template "python-flet" esta deprecado. Se usara "python-flet-polished" (estandar actual).\n'));
66
+ template = 'python-flet-polished';
67
+ }
68
+
69
+ // Si el template es auto, usar default segun skill
70
+ if (template === 'auto') {
71
+ template = TEMPLATE_DEFAULTS[skill] || 'web-pwa';
72
+ }
73
+
43
74
  const targetDir = path.join(process.cwd(), dir, name);
44
75
 
45
76
  // Leer version del CLI desde package.json
@@ -191,7 +222,7 @@ async function createG360Structure(projectPath, assetsDir) {
191
222
 
192
223
  const skillJsonPath = path.join(g360Dir, 'skill.json');
193
224
  if (!fs.existsSync(skillJsonPath)) {
194
- const exampleSkillPath = path.join(assetsDir, 'config/skills.json');
225
+ const exampleSkillPath = path.join(assetsDir, 'config/agent-skills.json');
195
226
  if (fs.existsSync(exampleSkillPath)) {
196
227
  await fs.copy(exampleSkillPath, skillJsonPath);
197
228
  }
@@ -1,9 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
+ import { walkProject } from '../lib/file-utils.js';
7
5
 
8
6
  const SEVERITY = {
9
7
  CRITICAL: 'critical',
@@ -30,8 +28,8 @@ const PYTHON_NAMING_RULES = {
30
28
  const GENERIC_NAMES = ['result', 'data', 'info', 'val', 'obj', 'tmp', 'aux', 'value', 'x', 'y', 'z', 'item', 'elem', 'entry', 'output', 'input', 'res', 'dt'];
31
29
 
32
30
  export async function lint(targetPath, options) {
33
- const { project = '.', level = 'all' } = options;
34
- const targetDir = path.join(process.cwd(), project);
31
+ const { level = 'all', project } = options;
32
+ const targetDir = project ? path.join(process.cwd(), project) : path.resolve(targetPath || '.');
35
33
 
36
34
  if (!fs.existsSync(targetDir)) {
37
35
  console.error(chalk.red(`❌ Directorio no encontrado: ${targetDir}`));
@@ -93,30 +91,10 @@ function checkNamingConventions(dir) {
93
91
  const jsFiles = [];
94
92
  const pyFiles = [];
95
93
 
96
- function walk(d) {
97
- const items = fs.readdirSync(d, { withFileTypes: true });
98
- for (const item of items) {
99
- if (item.name.startsWith('.')) continue;
100
- if (item.name === 'node_modules') continue;
101
- if (item.name === '__pycache__') continue;
102
- if (item.name === '.pytest_cache') continue;
103
- if (item.name === 'g360') continue;
104
-
105
- const fullPath = path.join(d, item.name);
106
- if (item.isDirectory()) {
107
- walk(fullPath);
108
- } else if (item.isFile()) {
109
- if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
110
- jsFiles.push(fullPath);
111
- }
112
- if (item.name.endsWith('.py') && !item.name.endsWith('__pycache__')) {
113
- pyFiles.push(fullPath);
114
- }
115
- }
116
- }
117
- }
118
-
119
- walk(dir);
94
+ walkProject(dir, {
95
+ onJs: (fullPath) => jsFiles.push(fullPath),
96
+ onPy: (fullPath) => pyFiles.push(fullPath),
97
+ });
120
98
 
121
99
  for (const file of jsFiles) {
122
100
  findings.push(...checkJsFileNaming(file, dir));
@@ -271,30 +249,10 @@ function checkDuplicateFunctions(dir) {
271
249
  const findings = [];
272
250
  const functionMap = new Map();
273
251
 
274
- function walk(d) {
275
- const items = fs.readdirSync(d, { withFileTypes: true });
276
- for (const item of items) {
277
- if (item.name.startsWith('.')) continue;
278
- if (item.name === 'node_modules') continue;
279
- if (item.name === '__pycache__') continue;
280
- if (item.name === '.pytest_cache') continue;
281
- if (item.name === 'g360') continue;
282
-
283
- const fullPath = path.join(d, item.name);
284
- if (item.isDirectory()) {
285
- walk(fullPath);
286
- } else if (item.isFile()) {
287
- if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
288
- extractFunctions(fullPath, 'js', functionMap);
289
- }
290
- if (item.name.endsWith('.py') && !item.name.endsWith('__pycache__')) {
291
- extractFunctions(fullPath, 'py', functionMap);
292
- }
293
- }
294
- }
295
- }
296
-
297
- walk(dir);
252
+ walkProject(dir, {
253
+ onJs: (fullPath) => extractFunctions(fullPath, 'js', functionMap),
254
+ onPy: (fullPath) => extractFunctions(fullPath, 'py', functionMap),
255
+ });
298
256
 
299
257
  for (const [name, locations] of functionMap) {
300
258
  if (locations.length > 1) {
@@ -411,37 +369,23 @@ function extractFunctions(filePath, lang, functionMap) {
411
369
  function checkSyntaxErrors(dir) {
412
370
  const findings = [];
413
371
 
414
- function walk(d) {
415
- const items = fs.readdirSync(d, { withFileTypes: true });
416
- for (const item of items) {
417
- if (item.name.startsWith('.')) continue;
418
- if (item.name === 'node_modules') continue;
419
- if (item.name === '__pycache__') continue;
420
- if (item.name === '.pytest_cache') continue;
421
- if (item.name === 'g360') continue;
422
-
423
- const fullPath = path.join(d, item.name);
424
- if (item.isDirectory()) {
425
- walk(fullPath);
426
- } else if (item.isFile()) {
427
- if (item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
428
- checkJsSyntax(fullPath, dir, findings);
429
- }
430
- if (item.name.endsWith('.py')) {
431
- checkPySyntax(fullPath, dir, findings);
432
- }
433
- }
434
- }
435
- }
372
+ walkProject(dir, {
373
+ onJs: (fullPath) => checkJsSyntax(fullPath, dir, findings),
374
+ onPy: (fullPath) => checkPySyntax(fullPath, dir, findings),
375
+ });
436
376
 
437
- walk(dir);
438
377
  return findings;
439
378
  }
440
379
 
441
380
  function checkJsSyntax(filePath, projectDir, findings) {
442
381
  const relPath = path.relative(projectDir, filePath);
382
+ const content = fs.readFileSync(filePath, 'utf8');
383
+
384
+ // Skip ES modules (import/export) — new Function() no puede parsearlos
385
+ if (/^(?:import|export)\s/m.test(content)) return;
386
+
443
387
  try {
444
- new Function(fs.readFileSync(filePath, 'utf8'));
388
+ new Function(content);
445
389
  } catch (error) {
446
390
  findings.push({
447
391
  severity: SEVERITY.CRITICAL,
@@ -492,6 +436,20 @@ function checkPySyntax(filePath, projectDir, findings) {
492
436
  while (indentStack.length > 1 && indentStack[indentStack.length - 1] > indent) {
493
437
  indentStack.pop();
494
438
  }
439
+ // Verificar que la indentacion actual este en el stack
440
+ if (indentStack.length > 0 && indent !== indentStack[indentStack.length - 1]) {
441
+ const indentUnit = indentStack.length > 1 ? indentStack[1] - indentStack[0] : 4;
442
+ if (indent % indentUnit !== 0) {
443
+ findings.push({
444
+ severity: SEVERITY.WARNING,
445
+ file: relPath,
446
+ type: 'indentation-error',
447
+ message: `Indentacion inconsistente en linea ${i + 1}: ${indent} espacios (esperado multiplo de ${indentUnit})`,
448
+ current: `${indent} espacios`,
449
+ recommended: `Multiplo de ${indentUnit} espacios`,
450
+ });
451
+ }
452
+ }
495
453
  }
496
454
  }
497
455
  }
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Comando: g360 scan <directorio>
4
- * Escanea un directorio para detectar archivos ERP válidos.
4
+ * Escanea un directorio para detectar archivos ERP validos.
5
5
  */
6
6
 
7
7
  import chalk from 'chalk';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { spawn } from 'child_process';
10
+ import { runPythonStdout, g360CorePath } from '../lib/python-runner.js';
11
11
 
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
13
 
@@ -17,8 +17,7 @@ export async function scan(directory, options) {
17
17
  console.log(chalk.blue(`\n🔍 Escaneando directorio: ${directory}`));
18
18
 
19
19
  const pyCode = `
20
- import sys
21
- sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
20
+ ${g360CorePath(__dirname)}
22
21
  from g360_core.scanner import find_erp_files_in_dir
23
22
  from pathlib import Path
24
23
  import json
@@ -33,22 +32,22 @@ print(json.dumps(result, indent=2))
33
32
  `;
34
33
 
35
34
  try {
36
- const result = await runPython(pyCode);
37
- const data = JSON.parse(result.stdout);
35
+ const stdout = await runPythonStdout(pyCode);
36
+ const data = JSON.parse(stdout);
38
37
 
39
38
  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}`));
39
+ console.log(chalk.green(` Validos: ${data.stats.valid}`));
40
+ console.log(chalk.red(` Invalidos: ${data.stats.invalid}`));
42
41
 
43
42
  if (data.valid.length > 0) {
44
- console.log(chalk.cyan('\n📋 Archivos válidos:'));
43
+ console.log(chalk.cyan('\n📋 Archivos validos:'));
45
44
  data.valid.forEach(f => {
46
45
  console.log(chalk.gray(` ${f.path} (${f.erp_type})`));
47
46
  });
48
47
  }
49
48
 
50
49
  if (data.invalid.length > 0) {
51
- console.log(chalk.yellow('\n⚠️ Archivos inválidos:'));
50
+ console.log(chalk.yellow('\n⚠️ Archivos invalidos:'));
52
51
  data.invalid.forEach(f => {
53
52
  console.log(chalk.red(` ${f.path}: ${f.error_msg}`));
54
53
  });
@@ -61,30 +60,3 @@ print(json.dumps(result, indent=2))
61
60
  process.exit(1);
62
61
  }
63
62
  }
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
- }
@@ -7,7 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
- import { spawn } from 'child_process';
10
+ import { runPython, g360CorePath } from '../lib/python-runner.js';
11
11
  import fs from 'fs-extra';
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -27,7 +27,6 @@ export async function validate(paths, options) {
27
27
  filesToCheck.push(p);
28
28
  }
29
29
  } else if (stat.isDirectory()) {
30
- const pattern = recursive ? '**/*' : '*';
31
30
  const extPattern = /\.(xls|xlsx|csv)$/i;
32
31
  const entries = await fs.readdir(p, { withFileTypes: true });
33
32
  for (const entry of entries) {
@@ -35,7 +34,6 @@ export async function validate(paths, options) {
35
34
  if (entry.isFile() && extPattern.test(entry.name)) {
36
35
  filesToCheck.push(fullPath);
37
36
  } else if (entry.isDirectory() && recursive) {
38
- // Simplificado: escaneo recursivo básico
39
37
  const sub = await fs.readdir(fullPath, { withFileTypes: true });
40
38
  for (const subEntry of sub) {
41
39
  if (subEntry.isFile() && extPattern.test(subEntry.name)) {
@@ -70,20 +68,19 @@ export async function validate(paths, options) {
70
68
  for (const res of results) {
71
69
  const status = res.valid ? chalk.green('✅') : chalk.red('❌');
72
70
  const filename = path.basename(res.path);
73
- console.log(`${status} ${filename} (${res.valid ? 'OK' : 'FALLÓ'})`);
71
+ console.log(`${status} ${filename} (${res.valid ? 'OK' : 'FALLO'})`);
74
72
  if (!res.valid && res.missing.length > 0) {
75
73
  console.log(chalk.gray(` Faltan: ${res.missing.join(', ')}`));
76
74
  }
77
75
  if (res.valid) validCount++;
78
76
  }
79
77
 
80
- console.log(chalk.gray(`\n📊 Resumen: ${validCount}/${results.length} archivos válidos`));
78
+ console.log(chalk.gray(`\n📊 Resumen: ${validCount}/${results.length} archivos validos`));
81
79
  }
82
80
 
83
81
  async function validateSingleFile(filepath) {
84
82
  const pyCode = `
85
- import sys
86
- sys.path.insert(0, '${path.join(__dirname, '..', 'py', 'src')}')
83
+ ${g360CorePath(__dirname)}
87
84
  from g360_core.scanner import ERPScanner
88
85
  from pathlib import Path
89
86
 
@@ -100,8 +97,8 @@ except Exception as e:
100
97
  `;
101
98
 
102
99
  try {
103
- const result = await runPython(pyCode);
104
- const lines = result.stdout.split('\n').filter(l => l.trim());
100
+ const { stdout } = await runPython(pyCode);
101
+ const lines = stdout.split('\n').filter(l => l.trim());
105
102
 
106
103
  let valid = false;
107
104
  let missing = [];
@@ -121,30 +118,3 @@ except Exception as e:
121
118
  return { path: filepath, valid: false, missing: [err.message] };
122
119
  }
123
120
  }
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
- }