g360-cli 1.14.0 → 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.
- package/package.json +71 -71
- package/src/assets/config/g360-skills.json +126 -0
- package/src/assets/config/project-types.json +8 -0
- package/src/assets/snippets/snippets.json +4 -0
- package/src/assets/templates/lit-web/src/index.js +1 -1
- package/src/assets/templates/python-cli/src/core/__init__.py +0 -0
- package/src/assets/templates/python-flet/pyproject.toml +1 -0
- package/src/assets/templates/python-flet/src/core/skill.json +1 -1
- package/src/assets/templates/python-flet-migrate/pyproject.toml +1 -3
- package/src/assets/templates/python-flet-polished/src/core/skill.json +16 -17
- package/src/assets/templates/solid-web/src/components/App.jsx +2 -2
- package/src/commands/clean.js +0 -12
- package/src/commands/docs.js +105 -1
- package/src/commands/ingest.js +11 -61
- package/src/commands/init.js +1 -1
- package/src/commands/lint.js +35 -77
- package/src/commands/scan.js +9 -37
- package/src/commands/validate.js +6 -36
- package/src/lib/file-utils.js +89 -2
- package/src/lib/python-runner.js +77 -0
- package/src/assets/templates/python-cli/package.json +0 -7
- /package/src/assets/config/{skills.json → agent-skills.json} +0 -0
package/package.json
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "g360-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/cli.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"g360": "src/cli.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"src/",
|
|
12
|
-
"!src/assets/templates/**/node_modules",
|
|
13
|
-
"!src/assets/templates/**/package-lock.json",
|
|
14
|
-
"!src/assets/templates/**/__pycache__",
|
|
15
|
-
"!src/assets/templates/**/.pytest_cache",
|
|
16
|
-
"py/",
|
|
17
|
-
"README.md",
|
|
18
|
-
"LICENSE",
|
|
19
|
-
"package.json"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"prepublishOnly": "node -e \"console.log('Use: npm install -g g360-cli')\"",
|
|
23
|
-
"test": "vitest",
|
|
24
|
-
"test:watch": "vitest --watch",
|
|
25
|
-
"test:ui": "vitest --ui",
|
|
26
|
-
"test:coverage": "vitest --coverage",
|
|
27
|
-
"build": "npm run build:portable",
|
|
28
|
-
"build:portable": "pkg . --targets node18-win-x64 --output dist/g360.exe",
|
|
29
|
-
"postinstall": "node -e \"try{require('child_process').execSync('python --version',{encoding:'utf8',stdio:'pipe'})}catch{console.log('⚠️ Python no detectado. Instala Python 3.11+ para funcionalidad completa.')}\""
|
|
30
|
-
},
|
|
31
|
-
"pkg": {
|
|
32
|
-
"assets": [
|
|
33
|
-
"src/assets/**/*",
|
|
34
|
-
"py/**/*"
|
|
35
|
-
],
|
|
36
|
-
"outputPath": "dist",
|
|
37
|
-
"scripts": [],
|
|
38
|
-
"targets": "node18-win-x64"
|
|
39
|
-
},
|
|
40
|
-
"keywords": [
|
|
41
|
-
"g360",
|
|
42
|
-
"cli",
|
|
43
|
-
"scaffolding",
|
|
44
|
-
"generator",
|
|
45
|
-
"erp",
|
|
46
|
-
"data-processing"
|
|
47
|
-
],
|
|
48
|
-
"author": "Carlos Cusi",
|
|
49
|
-
"license": "MIT",
|
|
50
|
-
"dependencies": {
|
|
51
|
-
"ajv": "^8.20.0",
|
|
52
|
-
"chalk": "^5.6.2",
|
|
53
|
-
"commander": "^14.0.3",
|
|
54
|
-
"fs-extra": "^11.3.4",
|
|
55
|
-
"inquirer": "^13.4.2",
|
|
56
|
-
"ora": "^9.4.0",
|
|
57
|
-
"winston": "^3.19.0"
|
|
58
|
-
},
|
|
59
|
-
"devDependencies": {
|
|
60
|
-
"@vitest/ui": "^4.1.6",
|
|
61
|
-
"pkg": "^5.8.1",
|
|
62
|
-
"vitest": "^4.1.6"
|
|
63
|
-
},
|
|
64
|
-
"engines": {
|
|
65
|
-
"node": ">=18.0.0"
|
|
66
|
-
},
|
|
67
|
-
"repository": {
|
|
68
|
-
"type": "git",
|
|
69
|
-
"url": "https://github.com/ccusi/g360-cli.git"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "g360-cli",
|
|
3
|
+
"version": "1.15.0",
|
|
4
|
+
"description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"g360": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"!src/assets/templates/**/node_modules",
|
|
13
|
+
"!src/assets/templates/**/package-lock.json",
|
|
14
|
+
"!src/assets/templates/**/__pycache__",
|
|
15
|
+
"!src/assets/templates/**/.pytest_cache",
|
|
16
|
+
"py/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"package.json"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"prepublishOnly": "node -e \"console.log('Use: npm install -g g360-cli')\"",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"test:watch": "vitest --watch",
|
|
25
|
+
"test:ui": "vitest --ui",
|
|
26
|
+
"test:coverage": "vitest --coverage",
|
|
27
|
+
"build": "npm run build:portable",
|
|
28
|
+
"build:portable": "pkg . --targets node18-win-x64 --output dist/g360.exe",
|
|
29
|
+
"postinstall": "node -e \"try{require('child_process').execSync('python --version',{encoding:'utf8',stdio:'pipe'})}catch{console.log('⚠️ Python no detectado. Instala Python 3.11+ para funcionalidad completa.')}\""
|
|
30
|
+
},
|
|
31
|
+
"pkg": {
|
|
32
|
+
"assets": [
|
|
33
|
+
"src/assets/**/*",
|
|
34
|
+
"py/**/*"
|
|
35
|
+
],
|
|
36
|
+
"outputPath": "dist",
|
|
37
|
+
"scripts": [],
|
|
38
|
+
"targets": "node18-win-x64"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"g360",
|
|
42
|
+
"cli",
|
|
43
|
+
"scaffolding",
|
|
44
|
+
"generator",
|
|
45
|
+
"erp",
|
|
46
|
+
"data-processing"
|
|
47
|
+
],
|
|
48
|
+
"author": "Carlos Cusi",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ajv": "^8.20.0",
|
|
52
|
+
"chalk": "^5.6.2",
|
|
53
|
+
"commander": "^14.0.3",
|
|
54
|
+
"fs-extra": "^11.3.4",
|
|
55
|
+
"inquirer": "^13.4.2",
|
|
56
|
+
"ora": "^9.4.0",
|
|
57
|
+
"winston": "^3.19.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@vitest/ui": "^4.1.6",
|
|
61
|
+
"pkg": "^5.8.1",
|
|
62
|
+
"vitest": "^4.1.6"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18.0.0"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "https://github.com/ccusi/g360-cli.git"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -318,6 +318,132 @@
|
|
|
318
318
|
"kpi-glow-cards",
|
|
319
319
|
"rotating-logger"
|
|
320
320
|
]
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "react-web",
|
|
324
|
+
"description": "Aplicaciones web con React/JSX - estilo G360 moderno",
|
|
325
|
+
"device": "pc",
|
|
326
|
+
"colors": {
|
|
327
|
+
"bg": "#0b1220",
|
|
328
|
+
"surface": "#1a2332",
|
|
329
|
+
"accent": "#00d084",
|
|
330
|
+
"text": "#f0f4f8",
|
|
331
|
+
"muted": "#94a3b8",
|
|
332
|
+
"success": "#22c55e",
|
|
333
|
+
"warning": "#f59e0b",
|
|
334
|
+
"error": "#ef4444"
|
|
335
|
+
},
|
|
336
|
+
"effects": {
|
|
337
|
+
"glassmorphism": true,
|
|
338
|
+
"blur": "12px",
|
|
339
|
+
"rounded": "12px"
|
|
340
|
+
},
|
|
341
|
+
"signature": {
|
|
342
|
+
"mode": "powered",
|
|
343
|
+
"text": "powered by G360"
|
|
344
|
+
},
|
|
345
|
+
"framework": "react"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "solid-web",
|
|
349
|
+
"description": "Aplicaciones web con SolidJS - estilo G360 moderno",
|
|
350
|
+
"device": "pc",
|
|
351
|
+
"colors": {
|
|
352
|
+
"bg": "#0b1220",
|
|
353
|
+
"surface": "#1a2332",
|
|
354
|
+
"accent": "#00d084",
|
|
355
|
+
"text": "#f0f4f8",
|
|
356
|
+
"muted": "#94a3b8",
|
|
357
|
+
"success": "#22c55e",
|
|
358
|
+
"warning": "#f59e0b",
|
|
359
|
+
"error": "#ef4444"
|
|
360
|
+
},
|
|
361
|
+
"effects": {
|
|
362
|
+
"glassmorphism": true,
|
|
363
|
+
"blur": "12px",
|
|
364
|
+
"rounded": "12px"
|
|
365
|
+
},
|
|
366
|
+
"signature": {
|
|
367
|
+
"mode": "powered",
|
|
368
|
+
"text": "powered by G360"
|
|
369
|
+
},
|
|
370
|
+
"framework": "solid"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"name": "svelte-web",
|
|
374
|
+
"description": "Aplicaciones web con Svelte/SvelteKit - estilo G360 moderno",
|
|
375
|
+
"device": "pc",
|
|
376
|
+
"colors": {
|
|
377
|
+
"bg": "#0b1220",
|
|
378
|
+
"surface": "#1a2332",
|
|
379
|
+
"accent": "#00d084",
|
|
380
|
+
"text": "#f0f4f8",
|
|
381
|
+
"muted": "#94a3b8",
|
|
382
|
+
"success": "#22c55e",
|
|
383
|
+
"warning": "#f59e0b",
|
|
384
|
+
"error": "#ef4444"
|
|
385
|
+
},
|
|
386
|
+
"effects": {
|
|
387
|
+
"glassmorphism": true,
|
|
388
|
+
"blur": "12px",
|
|
389
|
+
"rounded": "12px"
|
|
390
|
+
},
|
|
391
|
+
"signature": {
|
|
392
|
+
"mode": "powered",
|
|
393
|
+
"text": "powered by G360"
|
|
394
|
+
},
|
|
395
|
+
"framework": "svelte"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"name": "lit-web",
|
|
399
|
+
"description": "Aplicaciones web con Lit Web Components - estilo G360 moderno",
|
|
400
|
+
"device": "pc",
|
|
401
|
+
"colors": {
|
|
402
|
+
"bg": "#0b1220",
|
|
403
|
+
"surface": "#1a2332",
|
|
404
|
+
"accent": "#00d084",
|
|
405
|
+
"text": "#f0f4f8",
|
|
406
|
+
"muted": "#94a3b8",
|
|
407
|
+
"success": "#22c55e",
|
|
408
|
+
"warning": "#f59e0b",
|
|
409
|
+
"error": "#ef4444"
|
|
410
|
+
},
|
|
411
|
+
"effects": {
|
|
412
|
+
"glassmorphism": true,
|
|
413
|
+
"blur": "12px",
|
|
414
|
+
"rounded": "12px"
|
|
415
|
+
},
|
|
416
|
+
"signature": {
|
|
417
|
+
"mode": "powered",
|
|
418
|
+
"text": "powered by G360"
|
|
419
|
+
},
|
|
420
|
+
"framework": "lit"
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"name": "customtkinter",
|
|
424
|
+
"description": "Aplicaciones desktop con CustomTkinter - estilo G360 moderno",
|
|
425
|
+
"device": "pc",
|
|
426
|
+
"colors": {
|
|
427
|
+
"bg": "#0b1220",
|
|
428
|
+
"surface": "#1a2332",
|
|
429
|
+
"accent": "#00d084",
|
|
430
|
+
"text": "#f0f4f8",
|
|
431
|
+
"muted": "#94a3b8",
|
|
432
|
+
"success": "#22c55e",
|
|
433
|
+
"warning": "#f59e0b",
|
|
434
|
+
"error": "#ef4444"
|
|
435
|
+
},
|
|
436
|
+
"effects": {
|
|
437
|
+
"glassmorphism": true,
|
|
438
|
+
"blur": "12px",
|
|
439
|
+
"rounded": "12px"
|
|
440
|
+
},
|
|
441
|
+
"signature": {
|
|
442
|
+
"mode": "powered",
|
|
443
|
+
"text": "powered by G360"
|
|
444
|
+
},
|
|
445
|
+
"framework": "customtkinter",
|
|
446
|
+
"portable": true
|
|
321
447
|
}
|
|
322
448
|
]
|
|
323
449
|
}
|
|
@@ -54,6 +54,14 @@
|
|
|
54
54
|
"description": "Excel VBA macros",
|
|
55
55
|
"framework": "vba",
|
|
56
56
|
"features": ["excel", "macros", "forms"]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "python-customtkinter",
|
|
60
|
+
"name": "Python CustomTkinter Desktop",
|
|
61
|
+
"description": "Python desktop app with CustomTkinter framework",
|
|
62
|
+
"framework": "customtkinter",
|
|
63
|
+
"features": ["desktop", "gui", "cross-platform"],
|
|
64
|
+
"portable": true
|
|
57
65
|
}
|
|
58
66
|
]
|
|
59
67
|
}
|
|
@@ -45,21 +45,25 @@
|
|
|
45
45
|
{
|
|
46
46
|
"name": "g360-header",
|
|
47
47
|
"description": "Standard G360 header",
|
|
48
|
+
"language": "html",
|
|
48
49
|
"code": "<header class='g360-header'>...</header>"
|
|
49
50
|
},
|
|
50
51
|
{
|
|
51
52
|
"name": "g360-button",
|
|
52
53
|
"description": "G360 styled button",
|
|
54
|
+
"language": "html",
|
|
53
55
|
"code": "<button class='g360-btn'>Click</button>"
|
|
54
56
|
},
|
|
55
57
|
{
|
|
56
58
|
"name": "g360-card",
|
|
57
59
|
"description": "G360 card component",
|
|
60
|
+
"language": "html",
|
|
58
61
|
"code": "<div class='g360-card'>...</div>"
|
|
59
62
|
},
|
|
60
63
|
{
|
|
61
64
|
"name": "g360-badge",
|
|
62
65
|
"description": "G360 status badge",
|
|
66
|
+
"language": "html",
|
|
63
67
|
"code": "<span class='g360-badge g360-badge-success'>Active</span>"
|
|
64
68
|
},
|
|
65
69
|
{
|
|
File without changes
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "flet-desktop",
|
|
3
|
-
"description": "G360 Flet
|
|
2
|
+
"name": "flet-desktop-polished",
|
|
3
|
+
"description": "G360 Flet Polished - Plantilla base con patrones de UI avanzada",
|
|
4
4
|
"framework": "flet",
|
|
5
|
-
"version": "1.
|
|
6
|
-
"python": ">=3.11
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"python": ">=3.11",
|
|
7
7
|
"colors": {
|
|
8
8
|
"bg": "#0b1220",
|
|
9
|
-
"surface": "#
|
|
10
|
-
"accent": "#
|
|
11
|
-
"text": "#
|
|
12
|
-
"muted": "#
|
|
13
|
-
"success": "#
|
|
9
|
+
"surface": "#1a2333",
|
|
10
|
+
"accent": "#34d399",
|
|
11
|
+
"text": "#f0f6fc",
|
|
12
|
+
"muted": "#8b949e",
|
|
13
|
+
"success": "#34d399",
|
|
14
14
|
"warning": "#f59e0b",
|
|
15
15
|
"error": "#ef4444"
|
|
16
16
|
},
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"rounded": "12px"
|
|
21
21
|
},
|
|
22
22
|
"signature": {
|
|
23
|
-
"mode": "
|
|
24
|
-
"text": "G360
|
|
23
|
+
"mode": "powered",
|
|
24
|
+
"text": "powered by G360"
|
|
25
25
|
},
|
|
26
26
|
"build": {
|
|
27
27
|
"tool": "flet",
|
|
@@ -29,13 +29,12 @@
|
|
|
29
29
|
"name": "G360-App"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"flet": ">=0.
|
|
32
|
+
"flet": ">=0.28.3"
|
|
33
33
|
},
|
|
34
34
|
"guidelines": {
|
|
35
|
-
"architecture": "src/core (logica), src/ui (componentes), src/
|
|
36
|
-
"naming": "PascalCase para clases UI, camelCase para metodos
|
|
37
|
-
"state_management": "Usar atributos de clase
|
|
38
|
-
"threading": "Usar threading.
|
|
39
|
-
"window_config": "width=1350, height=900, resizable=True, theme_mode=DARK"
|
|
35
|
+
"architecture": "src/core (logica), src/ui (componentes), src/config (theme)",
|
|
36
|
+
"naming": "PascalCase para clases UI, camelCase para metodos",
|
|
37
|
+
"state_management": "Usar atributos de clase, hash diff para cache",
|
|
38
|
+
"threading": "Usar threading.Lock() para operaciones concurrentes"
|
|
40
39
|
}
|
|
41
40
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @version 1.0.0
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export default function App() {
|
|
8
|
+
export default function App(props) {
|
|
9
9
|
const titulo = 'Mi Proyecto G360';
|
|
10
10
|
|
|
11
11
|
return (
|
|
@@ -14,7 +14,7 @@ export default function App() {
|
|
|
14
14
|
<h1>{titulo}</h1>
|
|
15
15
|
</header>
|
|
16
16
|
<main>
|
|
17
|
-
|
|
17
|
+
{props.children}
|
|
18
18
|
</main>
|
|
19
19
|
</div>
|
|
20
20
|
);
|
package/src/commands/clean.js
CHANGED
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
10
|
import { getAllFiles } from '../lib/file-utils.js';
|
|
12
11
|
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
15
12
|
const DEAD_PATTERNS = [
|
|
16
13
|
'backup', 'backup-old', 'backups',
|
|
17
14
|
'temp', 'tmp', '.tmp',
|
|
@@ -292,15 +289,6 @@ async function findOrphanFiles(projectDir, allFiles) {
|
|
|
292
289
|
|
|
293
290
|
function analyzeFileOrganization(files) {
|
|
294
291
|
const misplaced = [];
|
|
295
|
-
const structureRules = {
|
|
296
|
-
'src/components': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
|
|
297
|
-
'src/pages': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
|
|
298
|
-
'src/utils': ['.js', '.ts'],
|
|
299
|
-
'src/hooks': ['.js', '.ts'],
|
|
300
|
-
'src/services': ['.js', '.ts'],
|
|
301
|
-
'src/styles': ['.css', '.scss', '.less'],
|
|
302
|
-
'src/assets': ['.png', '.jpg', '.svg', '.gif', '.webp']
|
|
303
|
-
};
|
|
304
292
|
|
|
305
293
|
const components = files.filter(f => {
|
|
306
294
|
const name = path.basename(f);
|
package/src/commands/docs.js
CHANGED
|
@@ -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, '..', '
|
|
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: [],
|
package/src/commands/ingest.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
import {
|
|
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
|
|
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
|
|
44
|
+
console.log(chalk.green(` Encontrados ${filepaths.length} archivos validos`));
|
|
45
45
|
} else {
|
|
46
|
-
console.error(chalk.red(`❌ Ruta no
|
|
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
|
-
|
|
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
|
|
106
|
-
const lines =
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
}
|
package/src/commands/init.js
CHANGED
|
@@ -222,7 +222,7 @@ async function createG360Structure(projectPath, assetsDir) {
|
|
|
222
222
|
|
|
223
223
|
const skillJsonPath = path.join(g360Dir, 'skill.json');
|
|
224
224
|
if (!fs.existsSync(skillJsonPath)) {
|
|
225
|
-
const exampleSkillPath = path.join(assetsDir, 'config/skills.json');
|
|
225
|
+
const exampleSkillPath = path.join(assetsDir, 'config/agent-skills.json');
|
|
226
226
|
if (fs.existsSync(exampleSkillPath)) {
|
|
227
227
|
await fs.copy(exampleSkillPath, skillJsonPath);
|
|
228
228
|
}
|
package/src/commands/lint.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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(
|
|
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
|
}
|
package/src/commands/scan.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
37
|
-
const data = JSON.parse(
|
|
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(`
|
|
41
|
-
console.log(chalk.red(`
|
|
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
|
|
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
|
|
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
|
-
}
|
package/src/commands/validate.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
import {
|
|
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' : '
|
|
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
|
|
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
|
-
|
|
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
|
|
104
|
-
const lines =
|
|
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
|
-
}
|
package/src/lib/file-utils.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
|
-
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', 'out', '.nuxt', 'coverage', '.cache', '.svelte-kit', '__pycache__', '.pytest_cache'];
|
|
4
|
+
const IGNORE_DIRS = ['node_modules', '.git', 'dist', 'build', '.next', 'out', '.nuxt', 'coverage', '.cache', '.svelte-kit', '__pycache__', '.pytest_cache', 'g360'];
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Obtiene todos los archivos relativos de un directorio (recursivo)
|
|
8
|
+
* @param {string} dir - Directorio raiz
|
|
9
|
+
* @param {string} baseDir - Directorio base para paths relativos
|
|
10
|
+
* @returns {Promise<string[]>} Lista de paths relativos
|
|
11
|
+
*/
|
|
6
12
|
export async function getAllFiles(dir, baseDir = dir) {
|
|
7
13
|
const files = [];
|
|
8
14
|
|
|
@@ -11,7 +17,7 @@ export async function getAllFiles(dir, baseDir = dir) {
|
|
|
11
17
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
18
|
|
|
13
19
|
for (const item of items) {
|
|
14
|
-
if (IGNORE_DIRS.includes(item.name)) continue;
|
|
20
|
+
if (IGNORE_DIRS.includes(item.name) || item.name.startsWith('.')) continue;
|
|
15
21
|
|
|
16
22
|
const fullPath = path.join(dir, item.name);
|
|
17
23
|
const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
|
|
@@ -25,3 +31,84 @@ export async function getAllFiles(dir, baseDir = dir) {
|
|
|
25
31
|
|
|
26
32
|
return files;
|
|
27
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Walk recursivo de proyecto con callbacks por tipo de archivo
|
|
37
|
+
* Consolidacion de los walk() duplicados en lint.js, docs.js, clean.js
|
|
38
|
+
* @param {string} dir - Directorio a escanear
|
|
39
|
+
* @param {Object} callbacks - Callbacks por extension
|
|
40
|
+
* @param {Function} callbacks.onJs - Callback para archivos .js/.jsx
|
|
41
|
+
* @param {Function} callbacks.onPy - Callback para archivos .py
|
|
42
|
+
* @param {Function} callbacks.onFile - Callback para cualquier archivo (recibe fullPath, relativePath)
|
|
43
|
+
* @param {Function} callbacks.onDir - Callback para directorios (recibe relativePath)
|
|
44
|
+
* @param {Object} options - Opciones extras
|
|
45
|
+
* @param {number} options.maxDepth - Profundidad maxima (default: 10)
|
|
46
|
+
* @param {string[]} options.skipDirs - Directorios adicionales a ignorar
|
|
47
|
+
*/
|
|
48
|
+
export function walkProject(dir, callbacks, options = {}) {
|
|
49
|
+
const { maxDepth = 10, skipDirs = [] } = options;
|
|
50
|
+
const allSkip = [...IGNORE_DIRS, ...skipDirs];
|
|
51
|
+
|
|
52
|
+
function walk(d, depth) {
|
|
53
|
+
if (depth > maxDepth) return;
|
|
54
|
+
const items = fs.readdirSync(d, { withFileTypes: true });
|
|
55
|
+
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
if (item.name.startsWith('.') || allSkip.includes(item.name)) continue;
|
|
58
|
+
|
|
59
|
+
const fullPath = path.join(d, item.name);
|
|
60
|
+
const relPath = path.relative(dir, fullPath).replace(/\\/g, '/');
|
|
61
|
+
|
|
62
|
+
if (item.isDirectory()) {
|
|
63
|
+
if (callbacks.onDir) callbacks.onDir(relPath, fullPath);
|
|
64
|
+
walk(fullPath, depth + 1);
|
|
65
|
+
} else if (item.isFile()) {
|
|
66
|
+
if (callbacks.onFile) callbacks.onFile(fullPath, relPath, item.name);
|
|
67
|
+
|
|
68
|
+
if (item.name.endsWith('.js') && !item.name.endsWith('.test.js') && !item.name.endsWith('.spec.js')) {
|
|
69
|
+
if (callbacks.onJs) callbacks.onJs(fullPath, relPath);
|
|
70
|
+
}
|
|
71
|
+
if (item.name.endsWith('.jsx') && !item.name.endsWith('.test.jsx')) {
|
|
72
|
+
if (callbacks.onJs) callbacks.onJs(fullPath, relPath);
|
|
73
|
+
}
|
|
74
|
+
if (item.name.endsWith('.ts') && !item.name.endsWith('.test.ts')) {
|
|
75
|
+
if (callbacks.onJs) callbacks.onJs(fullPath, relPath);
|
|
76
|
+
}
|
|
77
|
+
if (item.name.endsWith('.tsx') && !item.name.endsWith('.test.tsx')) {
|
|
78
|
+
if (callbacks.onJs) callbacks.onJs(fullPath, relPath);
|
|
79
|
+
}
|
|
80
|
+
if (item.name.endsWith('.py')) {
|
|
81
|
+
if (callbacks.onPy) callbacks.onPy(fullPath, relPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
walk(dir, 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Obtiene archivos JS/TS de un proyecto (sin tests)
|
|
92
|
+
* @param {string} dir - Directorio del proyecto
|
|
93
|
+
* @returns {string[]} Lista de paths absolutos
|
|
94
|
+
*/
|
|
95
|
+
export function getJsFiles(dir) {
|
|
96
|
+
const files = [];
|
|
97
|
+
walkProject(dir, {
|
|
98
|
+
onJs: (fullPath) => files.push(fullPath),
|
|
99
|
+
});
|
|
100
|
+
return files;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Obtiene archivos Python de un proyecto (sin __init__.py)
|
|
105
|
+
* @param {string} dir - Directorio del proyecto
|
|
106
|
+
* @returns {string[]} Lista de paths absolutos
|
|
107
|
+
*/
|
|
108
|
+
export function getPyFiles(dir) {
|
|
109
|
+
const files = [];
|
|
110
|
+
walkProject(dir, {
|
|
111
|
+
onPy: (fullPath) => files.push(fullPath),
|
|
112
|
+
});
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file python-runner.js
|
|
3
|
+
* @description Runner consolidado para ejecutar codigo Python desde Node.js
|
|
4
|
+
* @author @carloscus
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ejecuta codigo Python y retorna stdout/stderr
|
|
13
|
+
* @param {string} code - Codigo Python a ejecutar
|
|
14
|
+
* @param {Object} options - Opciones opcionales
|
|
15
|
+
* @param {string} options.pythonPath - Ruta al interprete Python
|
|
16
|
+
* @param {number} options.timeout - Timeout en ms
|
|
17
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
18
|
+
*/
|
|
19
|
+
export function runPython(code, options = {}) {
|
|
20
|
+
const { pythonPath, timeout } = options;
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const pyExec = pythonPath || process.env.PYTHON || 'python3';
|
|
24
|
+
const proc = spawn(pyExec, ['-c', code], {
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let timer;
|
|
31
|
+
|
|
32
|
+
if (timeout) {
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
proc.kill();
|
|
35
|
+
reject(new Error(`Python timeout despues de ${timeout}ms`));
|
|
36
|
+
}, timeout);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
40
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
41
|
+
|
|
42
|
+
proc.on('close', (code) => {
|
|
43
|
+
if (timer) clearTimeout(timer);
|
|
44
|
+
if (code === 0) {
|
|
45
|
+
resolve({ stdout, stderr });
|
|
46
|
+
} else {
|
|
47
|
+
reject(new Error(stderr || `Python termino con codigo ${code}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on('error', (err) => {
|
|
52
|
+
if (timer) clearTimeout(timer);
|
|
53
|
+
reject(new Error(`No se pudo ejecutar Python: ${err.message}`));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ejecuta codigo Python y retorna solo stdout
|
|
60
|
+
* @param {string} code - Codigo Python a ejecutar
|
|
61
|
+
* @param {Object} options - Mismas opciones que runPython
|
|
62
|
+
* @returns {Promise<string>} stdout
|
|
63
|
+
*/
|
|
64
|
+
export async function runPythonStdout(code, options = {}) {
|
|
65
|
+
const { stdout } = await runPython(code, options);
|
|
66
|
+
return stdout;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Crea el prefijo de Python para importar desde src/py/src
|
|
71
|
+
* @param {string} commandDir - Directorio del comando que invoca (usar __dirname o import.meta)
|
|
72
|
+
* @returns {string} Lineas Python para sys.path.insert
|
|
73
|
+
*/
|
|
74
|
+
export function g360CorePath(commandDir) {
|
|
75
|
+
const pySrc = path.join(commandDir, '..', 'py', 'src').replace(/\\/g, '\\\\');
|
|
76
|
+
return `import sys\nsys.path.insert(0, '${pySrc}')`;
|
|
77
|
+
}
|
|
File without changes
|