g360-cli 1.4.0 → 1.5.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 (85) hide show
  1. package/README.md +122 -0
  2. package/package.json +11 -4
  3. package/src/assets/brand/brand.json +43 -0
  4. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_borde.png +0 -0
  5. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_borde.svg +9 -0
  6. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_solid.png +0 -0
  7. package/src/assets/brand/cipsa/logotypes/Logo_cipsa_solid.svg +9 -0
  8. package/src/assets/brand/g360/logotypes/logo-g360-dark.svg +19 -0
  9. package/src/assets/brand/g360/logotypes/logo-g360-light.svg +19 -0
  10. package/src/assets/brand/g360/logotypes/logo_g360_dark_b64.txt +1 -0
  11. package/src/assets/brand/g360/logotypes/logo_g360_light_b64.txt +1 -0
  12. package/src/assets/config/g360-skills.json +60 -0
  13. package/src/assets/snippets/snippets.json +293 -101
  14. package/src/assets/templates/lit-web/src/core/skill.json +12 -5
  15. package/src/assets/templates/python-cli/build-portable.bat +19 -9
  16. package/src/assets/templates/python-cli/src/core/skill.json +25 -6
  17. package/src/assets/templates/python-customtkinter/README.md +32 -31
  18. package/src/assets/templates/python-customtkinter/assets/images/logo-g360-dark.svg +19 -0
  19. package/src/assets/templates/python-customtkinter/assets/images/logo-g360-light.svg +19 -0
  20. package/src/assets/templates/python-customtkinter/build.bat +50 -0
  21. package/src/assets/templates/python-customtkinter/pyproject.toml +26 -0
  22. package/src/assets/templates/python-customtkinter/run.bat +11 -20
  23. package/src/assets/templates/python-customtkinter/src/core/g360_theme.py +101 -0
  24. package/src/assets/templates/python-customtkinter/src/core/skill.json +23 -10
  25. package/src/assets/templates/python-customtkinter/src/main.py +61 -81
  26. package/src/assets/templates/python-flet/README.md +52 -93
  27. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_borde.png +0 -0
  28. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_borde.svg +9 -0
  29. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_solid.png +0 -0
  30. package/src/assets/templates/python-flet/assets/images/Logo_cipsa_solid.svg +9 -0
  31. package/src/assets/templates/python-flet/assets/images/logo-g360-dark.svg +19 -0
  32. package/src/assets/templates/python-flet/assets/images/logo-g360-light.svg +19 -0
  33. package/src/assets/templates/python-flet/build.bat +36 -0
  34. package/src/assets/templates/python-flet/pyproject.toml +26 -0
  35. package/src/assets/templates/python-flet/run.bat +11 -72
  36. package/src/assets/templates/python-flet/src/core/__init__.py +0 -0
  37. package/src/assets/templates/python-flet/src/core/g360_theme.py +186 -0
  38. package/src/assets/templates/python-flet/src/core/skill.json +28 -8
  39. package/src/assets/templates/python-flet/src/export/__init__.py +0 -0
  40. package/src/assets/templates/python-flet/src/main.py +113 -68
  41. package/src/assets/templates/python-flet/src/test_app.py +41 -37
  42. package/src/assets/templates/python-flet/src/ui/__init__.py +0 -0
  43. package/src/assets/templates/python-flet-migrate/README.md +70 -0
  44. package/src/assets/templates/python-flet-migrate/assets/images/logo-g360-dark.svg +19 -0
  45. package/src/assets/templates/python-flet-migrate/assets/images/logo-g360-light.svg +19 -0
  46. package/src/assets/templates/python-flet-migrate/build.bat +36 -0
  47. package/src/assets/templates/python-flet-migrate/pyproject.toml +26 -0
  48. package/src/assets/templates/python-flet-migrate/run.bat +20 -0
  49. package/src/assets/templates/python-flet-migrate/src/core/__init__.py +0 -0
  50. package/src/assets/templates/python-flet-migrate/src/core/g360_theme.py +186 -0
  51. package/src/assets/templates/python-flet-migrate/src/core/skill.json +34 -0
  52. package/src/assets/templates/python-flet-migrate/src/main.py +71 -42
  53. package/src/assets/templates/solid-web/src/core/skill.json +12 -5
  54. package/src/assets/templates/svelte-web/src/core/skill.json +12 -5
  55. package/src/assets/templates/web-pwa/index.html +2 -7
  56. package/src/assets/templates/web-pwa/public/favicon.svg +4 -0
  57. package/src/assets/templates/web-pwa/public/manifest.json +11 -0
  58. package/src/assets/templates/web-pwa/src/App.jsx +22 -0
  59. package/src/assets/templates/web-pwa/src/components/FooterSignature.jsx +9 -0
  60. package/src/assets/templates/web-pwa/src/components/Header.jsx +12 -0
  61. package/src/assets/templates/web-pwa/src/components/LoadingOverlay.jsx +10 -0
  62. package/src/assets/templates/web-pwa/src/core/skill.json +25 -0
  63. package/src/assets/templates/web-pwa/src/main.jsx +9 -0
  64. package/src/assets/templates/web-pwa/src/styles/main.css +133 -0
  65. package/src/assets/templates/web-pwa/vite.config.js +12 -0
  66. package/src/cli.js +4 -4
  67. package/src/commands/bring.js +81 -6
  68. package/src/commands/list.js +79 -9
  69. package/src/lib/asset-validator.js +145 -0
  70. package/src/lib/auditor.js +48 -0
  71. package/src/lib/global-config.js +260 -0
  72. package/src/lib/i18n.js +238 -0
  73. package/src/lib/logger.js +100 -0
  74. package/src/lib/manifest.js +70 -1
  75. package/src/lib/validator.js +39 -0
  76. package/src/schemas/skills-schema.json +1 -0
  77. package/src/schemas/snippets-schema.json +1 -0
  78. package/dist/g360.exe +0 -0
  79. package/src/assets/templates/python-customtkinter/build-portable.bat +0 -28
  80. package/src/assets/templates/python-customtkinter/requirements.txt +0 -1
  81. package/src/assets/templates/python-flet/build-portable.bat +0 -42
  82. package/src/assets/templates/python-flet/create_shortcut.vbs +0 -35
  83. package/src/assets/templates/python-flet/requirements.txt +0 -3
  84. package/src/assets/templates/web-pwa/app.js +0 -1
  85. package/src/assets/templates/web-pwa/styles.css +0 -28
@@ -0,0 +1,133 @@
1
+ :root {
2
+ --g360-bg: #0b1220;
3
+ --g360-surface: #151e2e;
4
+ --g360-accent: #00d084;
5
+ --g360-text: #f0f4f8;
6
+ --g360-muted: #94a3b8;
7
+ --g360-glass: rgba(21, 30, 46, 0.75);
8
+ --g360-blur: 16px;
9
+ --g360-rounded: 16px;
10
+ --g360-font: system-ui, -apple-system, sans-serif;
11
+ }
12
+
13
+ * { box-sizing: border-box; margin: 0; padding: 0; }
14
+
15
+ body {
16
+ font-family: var(--g360-font);
17
+ background: var(--g360-bg);
18
+ color: var(--g360-text);
19
+ line-height: 1.6;
20
+ -webkit-font-smoothing: antialiased;
21
+ }
22
+
23
+ .app {
24
+ min-height: 100vh;
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ .container {
30
+ max-width: 480px;
31
+ margin: 0 auto;
32
+ padding: 24px 16px;
33
+ flex: 1;
34
+ }
35
+
36
+ .g360-header {
37
+ background: var(--g360-surface);
38
+ border-bottom: 1px solid rgba(255,255,255,0.08);
39
+ padding: 16px 24px;
40
+ }
41
+
42
+ .header-content {
43
+ max-width: 480px;
44
+ margin: 0 auto;
45
+ display: flex;
46
+ align-items: baseline;
47
+ gap: 12px;
48
+ }
49
+
50
+ .logo {
51
+ font-size: 24px;
52
+ font-weight: 800;
53
+ color: var(--g360-accent);
54
+ }
55
+
56
+ .subtitle {
57
+ font-size: 14px;
58
+ color: var(--g360-muted);
59
+ }
60
+
61
+ h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
62
+
63
+ .muted { color: var(--g360-muted); font-size: 14px; }
64
+
65
+ .g360-footer {
66
+ text-align: center;
67
+ padding: 12px;
68
+ font-size: 11px;
69
+ color: var(--g360-muted);
70
+ background: var(--g360-surface);
71
+ border-top: 1px solid rgba(255,255,255,0.08);
72
+ }
73
+
74
+ .g360-loading-overlay {
75
+ position: fixed;
76
+ inset: 0;
77
+ background: rgba(11, 18, 32, 0.9);
78
+ display: flex;
79
+ flex-direction: column;
80
+ align-items: center;
81
+ justify-content: center;
82
+ gap: 16px;
83
+ z-index: 1000;
84
+ }
85
+
86
+ .g360-loading-spinner {
87
+ width: 40px;
88
+ height: 40px;
89
+ border: 3px solid var(--g360-surface);
90
+ border-top-color: var(--g360-accent);
91
+ border-radius: 50%;
92
+ animation: spin 0.8s linear infinite;
93
+ }
94
+
95
+ @keyframes spin { to { transform: rotate(360deg); } }
96
+
97
+ .g360-card {
98
+ background: var(--g360-surface);
99
+ border-radius: var(--g360-rounded);
100
+ padding: 16px;
101
+ border: 1px solid rgba(255,255,255,0.08);
102
+ }
103
+
104
+ .g360-glass {
105
+ background: var(--g360-glass);
106
+ backdrop-filter: blur(var(--g360-blur));
107
+ -webkit-backdrop-filter: blur(var(--g360-blur));
108
+ border: 1px solid rgba(255,255,255,0.1);
109
+ }
110
+
111
+ .g360-btn {
112
+ background: var(--g360-accent);
113
+ color: var(--g360-bg);
114
+ border: none;
115
+ border-radius: 8px;
116
+ padding: 10px 20px;
117
+ font-size: 14px;
118
+ font-weight: 700;
119
+ cursor: pointer;
120
+ transition: opacity 0.2s;
121
+ }
122
+
123
+ .g360-btn:hover { opacity: 0.85; }
124
+
125
+ @media (prefers-color-scheme: light) {
126
+ :root {
127
+ --g360-bg: #ffffff;
128
+ --g360-surface: #f5f5f5;
129
+ --g360-muted: #6b7280;
130
+ --g360-text: #1f2937;
131
+ --g360-glass: rgba(255, 255, 255, 0.75);
132
+ }
133
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000,
8
+ },
9
+ build: {
10
+ outDir: 'dist',
11
+ },
12
+ });
package/src/cli.js CHANGED
@@ -31,7 +31,7 @@ program
31
31
  .command('init')
32
32
  .argument('<name>', 'Project name')
33
33
  .option('-t, --template <type>', 'Project template type', 'web-pwa')
34
- .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop)', 'corporativo-movil')
34
+ .option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
35
35
  .option('-d, --dir <path>', 'Target directory', '.')
36
36
  .option('--dry-run', 'Preview without creating files')
37
37
  .option('--force', 'Overwrite existing files')
@@ -48,7 +48,7 @@ program
48
48
 
49
49
  program
50
50
  .command('bring')
51
- .argument('[asset]', 'Specific asset to bring (template/component/skill/all)')
51
+ .argument('[asset]', 'Asset to bring: brand, brand/cipsa, brand/g360, components, templates, skills, all')
52
52
  .option('-p, --path <path>', 'Target path', '.')
53
53
  .option('--dry-run', 'Preview without copying')
54
54
  .option('--force', 'Overwrite existing files')
@@ -56,7 +56,7 @@ program
56
56
 
57
57
  program
58
58
  .command('list')
59
- .argument('[type]', 'Type to list (templates/components/skills/all)', 'all')
59
+ .argument('[type]', 'Type to list (templates/components/skills/snippets/brands/all)', 'all')
60
60
  .option('--json', 'Output as JSON')
61
61
  .action(list);
62
62
 
@@ -100,7 +100,7 @@ program
100
100
  program
101
101
  .command('convert')
102
102
  .argument('[path]', 'Path to convert', '.')
103
- .option('-s, --skill <skill>', 'Skill to apply', 'corporativo-movil')
103
+ .option('-s, --skill <skill>', 'Skill to apply (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, cipsa, cipsa-movil)', 'corporativo-movil')
104
104
  .option('--dry-run', 'Preview without applying')
105
105
  .option('--restructure', 'Restructure project files')
106
106
  .option('--force', 'Force dangerous changes')
@@ -10,7 +10,7 @@ export async function bring(asset, options) {
10
10
  const { path: targetPath = '.', dryRun = false, force = false } = options;
11
11
  const targetDir = path.join(process.cwd(), targetPath);
12
12
  const assetsPath = path.join(__dirname, '../assets');
13
-
13
+
14
14
  console.log(chalk.bold.cyan('\n📦 G360 Asset Manager\n'));
15
15
 
16
16
  if (!asset || asset === 'all') {
@@ -19,14 +19,89 @@ export async function bring(asset, options) {
19
19
  }
20
20
 
21
21
  const assetPath = path.join(assetsPath, asset);
22
-
22
+
23
23
  if (!fs.existsSync(assetPath)) {
24
24
  console.error(chalk.red(`❌ Asset "${asset}" not found.`));
25
- console.log(chalk.gray('\nRun "g360 list assets" to see available assets.'));
25
+ console.log(chalk.gray('\nRun "g360 list all" to see available assets.'));
26
26
  return;
27
27
  }
28
28
 
29
29
  await copyAssets(assetPath, targetDir, dryRun, force);
30
+
31
+ if (asset.startsWith('brand')) {
32
+ const brandName = asset.split('/')[1];
33
+ if (brandName) {
34
+ await applyBrand(targetDir, brandName, dryRun);
35
+ }
36
+ }
37
+ }
38
+
39
+ async function applyBrand(targetDir, brandName, dryRun) {
40
+ const brandConfigPath = path.join(targetDir, 'g360', 'brand', 'brand.json');
41
+ if (!fs.existsSync(brandConfigPath)) {
42
+ console.log(chalk.yellow(`\n⚠ brand.json not found. Run 'g360 bring brand' first.`));
43
+ return;
44
+ }
45
+
46
+ let brandConfig;
47
+ try {
48
+ brandConfig = fs.readJsonSync(brandConfigPath);
49
+ } catch {
50
+ console.log(chalk.yellow(`\n⚠ Could not parse brand.json.`));
51
+ return;
52
+ }
53
+
54
+ const brand = brandConfig.brands?.[brandName];
55
+ if (!brand) {
56
+ console.log(chalk.yellow(`\n⚠ Brand "${brandName}" not found in brand.json`));
57
+ return;
58
+ }
59
+
60
+ const skillCandidates = [
61
+ path.join(targetDir, 'skill.json'),
62
+ path.join(targetDir, 'src', 'core', 'skill.json'),
63
+ ];
64
+
65
+ let skillPath = null;
66
+ for (const c of skillCandidates) {
67
+ if (fs.existsSync(c)) {
68
+ skillPath = c;
69
+ break;
70
+ }
71
+ }
72
+
73
+ if (!skillPath) {
74
+ console.log(chalk.yellow(`\n⚠ No skill.json found. Create one with 'g360 init'.`));
75
+ return;
76
+ }
77
+
78
+ if (dryRun) {
79
+ console.log(chalk.yellow(`\n📋 DRY RUN - Would update ${skillPath}`));
80
+ return;
81
+ }
82
+
83
+ const skill = fs.readJsonSync(skillPath);
84
+ skill.brand = brandName;
85
+
86
+ if (!skill.guidelines) skill.guidelines = {};
87
+ skill.guidelines.logo = brand.default_logo;
88
+ skill.guidelines.description = brand.description;
89
+
90
+ if (brand.signature) {
91
+ if (!skill.signature) skill.signature = {};
92
+ skill.signature.mode = brand.signature.mode;
93
+ skill.signature.text = brand.signature.text;
94
+ }
95
+
96
+ if (brand.primary_color) {
97
+ if (!skill.colors) skill.colors = {};
98
+ skill.colors.accent = brand.primary_color;
99
+ }
100
+
101
+ fs.writeJsonSync(skillPath, skill, { spaces: 2 });
102
+ console.log(chalk.green(`\n✅ Brand "${brandName}" applied to ${skillPath}`));
103
+ console.log(chalk.gray(` Logo: ${brand.default_logo}`));
104
+ console.log(chalk.gray(` Signature: ${brand.signature?.text || 'none'}`));
30
105
  }
31
106
 
32
107
  async function copyAssets(src, dest, dryRun, force) {
@@ -36,18 +111,18 @@ async function copyAssets(src, dest, dryRun, force) {
36
111
  }
37
112
 
38
113
  const progressBar = progress('Copying assets...');
39
-
114
+
40
115
  try {
41
116
  const items = fs.readdirSync(src);
42
117
  for (const item of items) {
43
118
  const srcPath = path.join(src, item);
44
119
  const destPath = path.join(dest, 'g360', item);
45
-
120
+
46
121
  if (fs.statSync(srcPath).isDirectory()) {
47
122
  await fs.copy(srcPath, destPath, { overwrite: force });
48
123
  }
49
124
  }
50
-
125
+
51
126
  progressBar.stop();
52
127
  console.log(chalk.green('\n✅ Assets copied successfully!\n'));
53
128
  } catch (error) {
@@ -14,13 +14,15 @@ export async function list(type, options) {
14
14
  const assets = {
15
15
  templates: [],
16
16
  components: [],
17
- skills: []
17
+ skills: [],
18
+ brands: []
18
19
  };
19
20
 
20
21
  const templatesPath = path.join(assetsPath, 'templates');
21
22
  const componentsPath = path.join(assetsPath, 'components');
22
- const skillsPath = path.join(assetsPath, 'skills');
23
- const enginePath = path.join(assetsPath, 'engine');
23
+ const configPath = path.join(assetsPath, 'config');
24
+ const snippetsPath = path.join(assetsPath, 'snippets');
25
+ const brandPath = path.join(assetsPath, 'brand');
24
26
 
25
27
  if (fs.existsSync(templatesPath)) {
26
28
  assets.templates = fs.readdirSync(templatesPath);
@@ -28,11 +30,53 @@ export async function list(type, options) {
28
30
  if (fs.existsSync(componentsPath)) {
29
31
  assets.components = fs.readdirSync(componentsPath).filter(f => f.endsWith('.jsx') || f.endsWith('.js'));
30
32
  }
31
- if (fs.existsSync(skillsPath)) {
32
- assets.skills = fs.readdirSync(skillsPath).filter(f => f.endsWith('.js') || f.endsWith('.mjs'));
33
+
34
+ if (fs.existsSync(configPath)) {
35
+ const g360SkillsPath = path.join(configPath, 'g360-skills.json');
36
+ if (fs.existsSync(g360SkillsPath)) {
37
+ try {
38
+ const skillsData = fs.readJsonSync(g360SkillsPath);
39
+ assets.skills = skillsData.skills.map(skill => ({
40
+ name: skill.name,
41
+ description: skill.description,
42
+ device: skill.device
43
+ }));
44
+ } catch (error) {
45
+ console.log(chalk.yellow(`Warning: Could not read skills config: ${error.message}`));
46
+ }
47
+ }
48
+ }
49
+
50
+ if (fs.existsSync(snippetsPath)) {
51
+ const snippetsJsonPath = path.join(snippetsPath, 'snippets.json');
52
+ if (fs.existsSync(snippetsJsonPath)) {
53
+ try {
54
+ const snippetsData = fs.readJsonSync(snippetsJsonPath);
55
+ assets.snippets = snippetsData.snippets.map(snippet => ({
56
+ name: snippet.name,
57
+ description: snippet.description,
58
+ language: snippet.language
59
+ }));
60
+ } catch (error) {
61
+ console.log(chalk.yellow(`Warning: Could not read snippets config: ${error.message}`));
62
+ }
63
+ }
33
64
  }
34
- if (fs.existsSync(enginePath)) {
35
- assets.engine = fs.readdirSync(enginePath).filter(f => f.startsWith('g360-skill-'));
65
+
66
+ if (fs.existsSync(brandPath)) {
67
+ const brandConfigPath = path.join(brandPath, 'brand.json');
68
+ if (fs.existsSync(brandConfigPath)) {
69
+ try {
70
+ const brandData = fs.readJsonSync(brandConfigPath);
71
+ assets.brands = Object.entries(brandData.brands || {}).map(([key, val]) => ({
72
+ name: key,
73
+ description: val.description,
74
+ logo: val.default_logo
75
+ }));
76
+ } catch (error) {
77
+ console.log(chalk.yellow(`Warning: Could not read brand config: ${error.message}`));
78
+ }
79
+ }
36
80
  }
37
81
 
38
82
  if (json) {
@@ -60,12 +104,38 @@ export async function list(type, options) {
60
104
 
61
105
  if (!type || type === 'all' || type === 'skills') {
62
106
  console.log(chalk.bold.yellow('\n⚡ Skills:'));
63
- if (assets.engine.length) {
64
- assets.engine.forEach(s => console.log(chalk.gray(` - ${s}`)));
107
+ if (assets.skills.length) {
108
+ assets.skills.forEach(skill => {
109
+ console.log(chalk.gray(` - ${skill.name}`));
110
+ console.log(chalk.gray(` ${skill.description}`));
111
+ console.log(chalk.gray(` Device: ${skill.device}`));
112
+ });
65
113
  } else {
66
114
  console.log(chalk.gray(' No skills found'));
67
115
  }
68
116
  }
69
117
 
118
+ if (!type || type === 'all' || type === 'brands') {
119
+ console.log(chalk.bold.yellow('\n🎨 Brands:'));
120
+ if (assets.brands.length) {
121
+ assets.brands.forEach(b => console.log(chalk.gray(` - ${b.name}`)));
122
+ } else {
123
+ console.log(chalk.gray(' No brands found'));
124
+ }
125
+ }
126
+
127
+ if (!type || type === 'all' || type === 'snippets') {
128
+ console.log(chalk.bold.yellow('\n📝 Snippets:'));
129
+ if (assets.snippets && assets.snippets.length) {
130
+ assets.snippets.forEach(snippet => {
131
+ console.log(chalk.gray(` - ${snippet.name}`));
132
+ console.log(chalk.gray(` ${snippet.description}`));
133
+ console.log(chalk.gray(` Language: ${snippet.language}`));
134
+ });
135
+ } else {
136
+ console.log(chalk.gray(' No snippets found'));
137
+ }
138
+ }
139
+
70
140
  console.log();
71
141
  }
@@ -0,0 +1,145 @@
1
+ import Ajv from 'ajv';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { logger } from './logger.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * @typedef {Object} ValidationResult
11
+ * @property {boolean} valid - Indica si la validación fue exitosa
12
+ * @property {string[]} errors - Lista de errores encontrados
13
+ */
14
+
15
+ /**
16
+ * Módulo de validación de assets con JSON Schema
17
+ * @namespace assetValidator
18
+ */
19
+ export const assetValidator = {
20
+ /**
21
+ * Instancia de AJV para validación de schemas
22
+ * @type {Ajv}
23
+ */
24
+ ajv: new Ajv({ allErrors: true }),
25
+
26
+ /**
27
+ * Cache de schemas compilados
28
+ * @type {Map<string, any>}
29
+ */
30
+ schemaCache: new Map(),
31
+
32
+ /**
33
+ * Carga y compila un schema JSON
34
+ * @param {string} schemaName - Nombre del schema a cargar
35
+ * @returns {Promise<any>} Schema compilado
36
+ * @private
37
+ */
38
+ async loadSchema(schemaName) {
39
+ if (this.schemaCache.has(schemaName)) {
40
+ return this.schemaCache.get(schemaName);
41
+ }
42
+
43
+ const schemaPath = path.join(__dirname, '../schemas', `${schemaName}.json`);
44
+
45
+ if (!fs.existsSync(schemaPath)) {
46
+ throw new Error(`Schema not found: ${schemaName}`);
47
+ }
48
+
49
+ const schema = await fs.readJson(schemaPath);
50
+ const compiled = this.ajv.compile(schema);
51
+
52
+ this.schemaCache.set(schemaName, compiled);
53
+ return compiled;
54
+ },
55
+
56
+ /**
57
+ * Valida un objeto contra un schema específico
58
+ * @param {string} schemaName - Nombre del schema a usar
59
+ * @param {Object} data - Datos a validar
60
+ * @returns {Promise<ValidationResult>} Resultado de la validación
61
+ * @example
62
+ * const result = await assetValidator.validate('skills-schema', skillsData);
63
+ * if (result.valid) {
64
+ * console.log('Skills válidos');
65
+ * } else {
66
+ * console.error('Errores:', result.errors);
67
+ * }
68
+ */
69
+ async validate(schemaName, data) {
70
+ try {
71
+ const validate = await this.loadSchema(schemaName);
72
+ const valid = validate(data);
73
+
74
+ if (valid) {
75
+ logger.debug(`Validation passed for schema: ${schemaName}`);
76
+ return { valid: true, errors: [] };
77
+ }
78
+
79
+ const errors = validate.errors.map(err =>
80
+ `${err.instancePath} ${err.message}`
81
+ );
82
+
83
+ logger.warn(`Validation failed for schema: ${schemaName}`, { errors });
84
+ return { valid: false, errors };
85
+ } catch (error) {
86
+ logger.error(`Error validating against schema: ${schemaName}`, { error: error.message });
87
+ return {
88
+ valid: false,
89
+ errors: [`Validation error: ${error.message}`]
90
+ };
91
+ }
92
+ },
93
+
94
+ /**
95
+ * Valida la configuración de skills
96
+ * @param {Object} skillsData - Datos de skills a validar
97
+ * @returns {Promise<ValidationResult>} Resultado de la validación
98
+ * @example
99
+ * const result = await assetValidator.validateSkills(skillsConfig);
100
+ */
101
+ async validateSkills(skillsData) {
102
+ return this.validate('skills-schema', skillsData);
103
+ },
104
+
105
+ /**
106
+ * Valida la configuración de snippets
107
+ * @param {Object} snippetsData - Datos de snippets a validar
108
+ * @returns {Promise<ValidationResult>} Resultado de la validación
109
+ * @example
110
+ * const result = await assetValidator.validateSnippets(snippetsConfig);
111
+ */
112
+ async validateSnippets(snippetsData) {
113
+ return this.validate('snippets-schema', snippetsData);
114
+ },
115
+
116
+ /**
117
+ * Valida un asset individual
118
+ * @param {string} assetType - Tipo de asset ('skill' | 'snippet')
119
+ * @param {Object} assetData - Datos del asset a validar
120
+ * @returns {Promise<ValidationResult>} Resultado de la validación
121
+ * @example
122
+ * const result = await assetValidator.validateAsset('skill', skillData);
123
+ */
124
+ async validateAsset(assetType, assetData) {
125
+ const schemaName = assetType === 'skill' ? 'skills-schema' : 'snippets-schema';
126
+
127
+ // Para validación individual, envolvemos el asset en la estructura esperada
128
+ const wrapper = assetType === 'skill'
129
+ ? { skills: [assetData] }
130
+ : { snippets: [assetData] };
131
+
132
+ const result = await this.validate(schemaName, wrapper);
133
+
134
+ return result;
135
+ },
136
+
137
+ /**
138
+ * Limpia el cache de schemas
139
+ * @returns {void}
140
+ */
141
+ clearCache() {
142
+ this.schemaCache.clear();
143
+ logger.debug('Schema cache cleared');
144
+ }
145
+ };
@@ -2,7 +2,37 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
 
5
+ /**
6
+ * @typedef {Object} AuditIssue
7
+ * @property {string} file - Archivo donde se encontró el issue
8
+ * @property {string} message - Mensaje descriptivo del issue
9
+ * @property {string} severity - Severidad del issue ('error' | 'warning')
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} AuditResult
14
+ * @property {number} passed - Cantidad de checks que pasaron
15
+ * @property {number} failed - Cantidad de checks que fallaron
16
+ * @property {number} warnings - Cantidad de advertencias
17
+ * @property {number} total - Cantidad total de checks
18
+ * @property {AuditIssue[]} issues - Lista de issues encontrados
19
+ */
20
+
21
+ /**
22
+ * Módulo de auditoría para proyectos G360
23
+ * @namespace auditor
24
+ */
5
25
  export const auditor = {
26
+ /**
27
+ * Ejecuta auditoría completa del proyecto
28
+ * @param {string} projectDir - Directorio del proyecto a auditar
29
+ * @param {Object} options - Opciones de auditoría
30
+ * @param {boolean} [options.verbose=false] - Mostrar información detallada
31
+ * @returns {Promise<AuditResult>} Resultado de la auditoría
32
+ * @example
33
+ * const result = await auditor.audit('/my/project', { verbose: true });
34
+ * console.log(`Passed: ${result.passed}, Failed: ${result.failed}`);
35
+ */
6
36
  async audit(projectDir, options = {}) {
7
37
  const { verbose = false } = options;
8
38
  const results = {
@@ -41,6 +71,12 @@ export const auditor = {
41
71
  return results;
42
72
  },
43
73
 
74
+ /**
75
+ * Verifica si existe el manifiesto del proyecto
76
+ * @param {string} projectDir - Directorio del proyecto
77
+ * @returns {Object} Resultado del check
78
+ * @private
79
+ */
44
80
  checkManifest(projectDir) {
45
81
  const manifestPath = path.join(projectDir, 'g360-manifest.json');
46
82
  if (fs.existsSync(manifestPath)) {
@@ -67,6 +103,12 @@ export const auditor = {
67
103
  return { status: 'warn', issue: { file: 'g360-manifest.json', message: 'No manifest found - run "g360 bring" to initialize' } };
68
104
  },
69
105
 
106
+ /**
107
+ * Verifica la estructura del proyecto
108
+ * @param {string} projectDir - Directorio del proyecto
109
+ * @returns {Object} Resultado del check
110
+ * @private
111
+ */
70
112
  checkStructure(projectDir) {
71
113
  const g360Dir = path.join(projectDir, 'g360');
72
114
  if (fs.existsSync(g360Dir)) {
@@ -94,6 +136,12 @@ export const auditor = {
94
136
  return { status: 'warn', issue: { file: 'g360/', message: 'No G360 assets found - run "g360 bring" to initialize assets' } };
95
137
  },
96
138
 
139
+ /**
140
+ * Verifica la configuración del proyecto
141
+ * @param {string} projectDir - Directorio del proyecto
142
+ * @returns {Object} Resultado del check
143
+ * @private
144
+ */
97
145
  checkConfig(projectDir) {
98
146
  const configPath = path.join(projectDir, 'g360', 'config');
99
147
  if (fs.existsSync(configPath)) {