g360-cli 1.15.1 → 1.16.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "g360-cli",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "CLI tool for bootstrapping G360 projects with standardized structure, assets, identity, and ERP data processing",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
@@ -54,6 +54,7 @@
54
54
  "fs-extra": "^11.3.4",
55
55
  "inquirer": "^13.4.2",
56
56
  "ora": "^9.4.0",
57
+ "pptxgenjs": "^4.0.1",
57
58
  "winston": "^3.19.0"
58
59
  },
59
60
  "devDependencies": {
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Layouts reutilizables para slides G360.
3
+ * Cada layout recibe (slide, theme) y retorna el slide modificado.
4
+ */
5
+
6
+ /**
7
+ * Cover slide — portada de la app
8
+ */
9
+ export function coverSlide(slide, data, theme) {
10
+ const { pptx, colors } = theme;
11
+ slide.addText(data.appName || 'Mi App G360', [0.5, 1.5, 9, 1.2], {
12
+ fontSize: 44, bold: true, color: colors.text, fontFace: 'Inter',
13
+ align: 'center', margin: 0,
14
+ });
15
+ slide.addText(data.description || 'Aplicacion de gestion y monitoreo', [0.5, 3.0, 9, 0.8], {
16
+ fontSize: 20, color: colors.textLight, fontFace: 'Inter',
17
+ align: 'center', margin: 0,
18
+ });
19
+ if (data.version) {
20
+ slide.addText(`v${data.version}`, [0.5, 4.0, 9, 0.5], {
21
+ fontSize: 14, color: colors.accent, fontFace: 'Inter',
22
+ align: 'center', margin: 0,
23
+ });
24
+ }
25
+ if (data.brand && data.brand === 'cipsa') {
26
+ slide.addText('CIPSA', [0.5, 10.5, 9, 0.5], {
27
+ fontSize: 12, color: colors.textLight, fontFace: 'Inter',
28
+ align: 'center', margin: 0,
29
+ });
30
+ }
31
+ // Linea decorativa inferior
32
+ slide.addShape(pptx.ShapeType.rect, [0, 13.5, 10, 0.05], {
33
+ fill: { color: colors.accent }, line: { color: colors.accent },
34
+ });
35
+ return slide;
36
+ }
37
+
38
+ /**
39
+ * Screenshot slide — con placeholder flexible para imagen
40
+ * data: { title, description, screenshotPath?, screenshotIndex? }
41
+ */
42
+ export function screenshotSlide(slide, data, theme) {
43
+ const { colors } = theme;
44
+ slide.addText(data.title || 'Captura de Pantalla', [0.5, 0.3, 9, 0.5], {
45
+ fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
46
+ });
47
+ if (data.description) {
48
+ slide.addText(data.description, [0.5, 0.85, 9, 0.4], {
49
+ fontSize: 12, color: colors.textLight, fontFace: 'Inter',
50
+ });
51
+ }
52
+ // Area de imagen (80% ancho, centro)
53
+ const imgX = 0.5, imgY = 1.4, imgW = 9, imgH = 9.5;
54
+ if (data.screenshotPath) {
55
+ slide.addImage({ path: data.screenshotPath, x: imgX, y: imgY, w: imgW, h: imgH });
56
+ } else {
57
+ // Placeholder con borde punteado
58
+ slide.addShape('rect', [imgX, imgY, imgW, imgH], {
59
+ fill: { color: colors.surface },
60
+ line: { color: colors.border, width: 2, dashType: 'dash' },
61
+ });
62
+ slide.addText('📷 Insertar screenshot aquí', [imgX, imgY + 4, imgW, 1], {
63
+ fontSize: 16, color: colors.textLight, fontFace: 'Inter',
64
+ align: 'center', valign: 'middle',
65
+ });
66
+ }
67
+ return slide;
68
+ }
69
+
70
+ /**
71
+ * Feature slide — icono + titulo + descripcion
72
+ */
73
+ export function featureSlide(slide, data, theme) {
74
+ const { colors } = theme;
75
+ slide.addText(data.title || 'Funcionalidad', [0.5, 0.3, 9, 0.6], {
76
+ fontSize: 22, bold: true, color: colors.text, fontFace: 'Inter',
77
+ });
78
+ // description como string unico (PptxGenJS no acepta array de strings plano)
79
+ const desc = data.description || '';
80
+ slide.addText(desc, [0.5, 1.0, 9, 1.5], { fontSize: 14, color: colors.text, fontFace: 'Inter' });
81
+ // bullets
82
+ if (data.bullets && Array.isArray(data.bullets) && data.bullets.length > 0) {
83
+ const bulletTexts = data.bullets.map(b => ({ text: b, options: { bullet: true } }));
84
+ slide.addText(bulletTexts, [0.5, 2.6, 9, data.bullets.length * 0.4], {
85
+ fontSize: 13, color: colors.text, fontFace: 'Inter',
86
+ });
87
+ }
88
+ return slide;
89
+ }
90
+
91
+ /**
92
+ * Workflow slide — pasos numerados con flechas
93
+ */
94
+ export function workflowSlide(slide, data, theme) {
95
+ const { colors } = theme;
96
+ slide.addText(data.title || 'Flujo de trabajo', [0.5, 0.3, 9, 0.5], {
97
+ fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
98
+ });
99
+ const steps = data.steps || [];
100
+ const stepHeight = 1.8;
101
+ const startY = 1.0;
102
+ steps.forEach((step, i) => {
103
+ const y = startY + i * stepHeight;
104
+ slide.addText(String(i + 1), [0.5, y, 0.5, 0.4], {
105
+ fontSize: 16, bold: true, color: colors.accent, fontFace: 'Inter',
106
+ });
107
+ slide.addText(step.title || '', [1.2, y, 3.5, 0.4], {
108
+ fontSize: 14, bold: true, color: colors.text, fontFace: 'Inter',
109
+ });
110
+ slide.addText(step.desc || '', [1.2, y + 0.4, 8, 0.6], {
111
+ fontSize: 12, color: colors.textLight, fontFace: 'Inter',
112
+ });
113
+ if (i < steps.length - 1) {
114
+ slide.addText('↓', [0.6, y + 0.85, 0.4, 0.4], {
115
+ fontSize: 18, color: colors.accent, fontFace: 'Inter',
116
+ });
117
+ }
118
+ });
119
+ return slide;
120
+ }
121
+
122
+ /**
123
+ * Architecture slide — diagrama por capas (core/ui/data)
124
+ */
125
+ export function architectureSlide(slide, data, theme) {
126
+ const { colors } = theme;
127
+ slide.addText(data.title || 'Arquitectura', [0.5, 0.3, 9, 0.5], {
128
+ fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
129
+ });
130
+ const layers = data.layers || [
131
+ { name: 'UI', desc: 'Dashboard, KPIs, Modals', color: colors.accent },
132
+ { name: 'Core', desc: 'Processor, Downloader, Models', color: colors.info },
133
+ { name: 'Data', desc: 'API S1, Catalogo, Cache', color: colors.violet },
134
+ ];
135
+ const layerH = 2.2;
136
+ const startY = 1.0;
137
+ layers.forEach((layer, i) => {
138
+ const y = startY + i * (layerH + 0.2);
139
+ slide.addShape('rect', [0.5, y, 9, layerH], {
140
+ fill: { color: layer.color + '20' },
141
+ line: { color: layer.color, width: 1.5 },
142
+ });
143
+ slide.addText(layer.name, [0.7, y + 0.3, 2.5, 0.5], {
144
+ fontSize: 16, bold: true, color: layer.color, fontFace: 'Inter',
145
+ });
146
+ slide.addText(layer.desc, [3.3, y + 0.35, 6, 0.4], {
147
+ fontSize: 13, color: colors.text, fontFace: 'Inter',
148
+ });
149
+ if (i < layers.length - 1) {
150
+ slide.addText('▼', [4.5, y + layerH + 0.05, 0.5, 0.2], {
151
+ fontSize: 10, color: colors.textLight, fontFace: 'Inter',
152
+ });
153
+ }
154
+ });
155
+ return slide;
156
+ }
157
+
158
+ /**
159
+ * Checklist slide — buenas practicas / resumen
160
+ */
161
+ export function checklistSlide(slide, data, theme) {
162
+ const { colors } = theme;
163
+ slide.addText(data.title || 'Resumen', [0.5, 0.3, 9, 0.5], {
164
+ fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
165
+ });
166
+ const items = data.items || [];
167
+ items.forEach((item, i) => {
168
+ const y = 0.9 + i * 0.55;
169
+ const checked = item.checked !== false;
170
+ slide.addText(checked ? '✓' : '○', [0.5, y, 0.4, 0.4], {
171
+ fontSize: 14, color: checked ? colors.success : colors.textLight, fontFace: 'Arial',
172
+ });
173
+ slide.addText(item.text || '', [1.0, y, 8.5, 0.4], {
174
+ fontSize: 13, color: colors.text, fontFace: 'Inter',
175
+ });
176
+ });
177
+ return slide;
178
+ }
179
+
180
+ /**
181
+ * KPI / metrics slide — para dashboards
182
+ */
183
+ export function kpiSlide(slide, data, theme) {
184
+ const { colors } = theme;
185
+ slide.addText(data.title || 'Indicadores clave', [0.5, 0.3, 9, 0.5], {
186
+ fontSize: 18, bold: true, color: colors.text, fontFace: 'Inter',
187
+ });
188
+ const kpis = data.kpis || [];
189
+ const cols = Math.min(kpis.length, 4);
190
+ const colW = 9 / cols;
191
+ kpis.forEach((kpi, i) => {
192
+ const x = 0.5 + i * colW + colW * 0.1;
193
+ const w = colW * 0.8;
194
+ slide.addShape('rect', [x, 1.0, w, 2.5], {
195
+ fill: { color: (kpi.color || colors.accent) + '15' },
196
+ line: { color: kpi.color || colors.accent, width: 1 },
197
+ });
198
+ slide.addText(kpi.value || '—', [x, 1.1, w, 0.9], {
199
+ fontSize: 28, bold: true, color: kpi.color || colors.accent, fontFace: 'JetBrains Mono',
200
+ align: 'center',
201
+ });
202
+ slide.addText(kpi.label || '', [x, 2.0, w, 0.3], {
203
+ fontSize: 11, color: colors.textLight, fontFace: 'Inter', align: 'center',
204
+ });
205
+ if (kpi.sub) {
206
+ slide.addText(kpi.sub, [x, 2.35, w, 0.3], {
207
+ fontSize: 10, color: colors.textLight, fontFace: 'Inter', align: 'center',
208
+ });
209
+ }
210
+ });
211
+ return slide;
212
+ }
213
+
214
+ /**
215
+ * Footer con branding G360
216
+ */
217
+ export function addFooter(slide, theme, slideNum, totalSlides) {
218
+ const { colors } = theme;
219
+ slide.addText('powered by G360', [7.5, 13.7, 2.2, 0.3], {
220
+ fontSize: 10, color: colors.textLight, fontFace: 'Inter', align: 'right',
221
+ });
222
+ slide.addText(`${slideNum} / ${totalSlides}`, [8.5, 13.9, 1, 0.25], {
223
+ fontSize: 9, color: colors.textLight, fontFace: 'Inter', align: 'right',
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Helper: agregar seccion header comun
229
+ */
230
+ export function addSectionHeader(slide, section, theme) {
231
+ const { colors } = theme;
232
+ slide.addText(section, [0.5, 0.15, 9, 0.35], {
233
+ fontSize: 10, bold: true, color: colors.accent, fontFace: 'Inter',
234
+ charSpacing: 2,
235
+ });
236
+ slide.addShape('rect', [0.5, 0.52, 9, 0.02], {
237
+ fill: { color: colors.accent }, line: { color: colors.accent },
238
+ });
239
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Layouts adicionales re-exportados desde base.js para compatibilidad.
3
+ */
4
+ export {
5
+ coverSlide,
6
+ screenshotSlide,
7
+ featureSlide,
8
+ workflowSlide,
9
+ architectureSlide,
10
+ checklistSlide,
11
+ kpiSlide,
12
+ addFooter,
13
+ addSectionHeader,
14
+ } from './base.js';
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Analisis de app G360 — extrae features, screens y flujos de la estructura estandarizada.
3
+ * Aprovecha las convenciones FLET-NAMING-CONVENTIONS.md para parseo deterministico.
4
+ */
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * Clases UI conocidas → nombre legible para el usuario.
10
+ */
11
+ const UI_CLASS_MAP = {
12
+ 'KpiCard': 'Indicadores KPI',
13
+ 'Dashboard': 'Dashboard',
14
+ 'WarehouseCard': 'Tarjeta de Almacen',
15
+ 'SearchOverlay': 'Buscador Flotante',
16
+ 'ExportModal': 'Exportar a Excel',
17
+ 'SkuDetailModal': 'Detalle de SKU',
18
+ 'TrasladosModal': 'Traslados entre Almacenes',
19
+ 'SinStockModal': 'Productos Sin Stock',
20
+ 'LineaSection': 'Seccion por Linea',
21
+ 'AppSidebar': 'Barra Lateral',
22
+ 'LoadingOverlay': 'Indicador de Carga',
23
+ 'HealthBadge': 'Estado de Salud',
24
+ };
25
+
26
+ /**
27
+ * Patrón para detectar metodos desde src/app.py.
28
+ */
29
+ const METHOD_PATTERN = /def\s+(_(?:setup|build|on|fetch|load|save|update|show|hide|toggle|validate|format)[_\w]*)/g;
30
+
31
+ /**
32
+ * Patrón para detectar imports de clases UI.
33
+ */
34
+ const IMPORT_UI_PATTERN = /from\s+src\.ui\.\w+\s+import\s+([\w,]+)/g;
35
+
36
+ /**
37
+ * Analiza la estructura de una app G360.
38
+ */
39
+ export async function analyzeApp(projectDir) {
40
+ const result = {
41
+ name: '',
42
+ version: '',
43
+ description: '',
44
+ brand: 'g360',
45
+ skill: '',
46
+ framework: '',
47
+ features: [],
48
+ screens: [],
49
+ workflows: [],
50
+ modules: [],
51
+ screenshots: [],
52
+ hasSource1: false,
53
+ hasSource2: false,
54
+ hasExport: false,
55
+ hasAutoRefresh: false,
56
+ hasSearch: false,
57
+ hasModals: [],
58
+ templates: [],
59
+ };
60
+
61
+ // 1. Leer skill.json
62
+ const skillPath = path.join(projectDir, 'skill.json');
63
+ if (await fs.pathExists(skillPath)) {
64
+ try {
65
+ const skill = await fs.readJson(skillPath);
66
+ result.name = skill.name || result.name;
67
+ result.description = skill.description || result.description;
68
+ result.brand = skill.brand || 'g360';
69
+ result.skill = skill.skill || skill.name || '';
70
+ result.framework = skill.framework || '';
71
+ result.version = skill.version || '';
72
+ result.events = skill.events || [];
73
+ } catch { /* ignorar */ }
74
+ }
75
+
76
+ // 2. Leer manifest
77
+ const manifestPath = path.join(projectDir, 'g360-manifest.json');
78
+ if (await fs.pathExists(manifestPath)) {
79
+ try {
80
+ const manifest = await fs.readJson(manifestPath);
81
+ result.name = manifest.name || result.name;
82
+ result.version = manifest.version || result.version;
83
+ } catch { /* ignorar */ }
84
+ }
85
+
86
+ // 3. Escanear src/ui/ para clases UI conocidas
87
+ const uiDir = path.join(projectDir, 'src', 'ui');
88
+ if (await fs.pathExists(uiDir)) {
89
+ const uiFiles = await fs.readdir(uiDir);
90
+ for (const file of uiFiles) {
91
+ if (file === '__pycache__' || file.endsWith('.pyc')) continue;
92
+ const filePath = path.join(uiDir, file);
93
+ if (await fs.pathExists(filePath)) {
94
+ const content = await fs.readFile(filePath, 'utf-8');
95
+ const className = file.replace('.py', '');
96
+ const displayName = UI_CLASS_MAP[className] || className;
97
+ result.features.push({
98
+ name: className,
99
+ display: displayName,
100
+ file,
101
+ path: filePath,
102
+ });
103
+ }
104
+ }
105
+ }
106
+
107
+ // 4. Escanear modals
108
+ const modalsDir = path.join(projectDir, 'src', 'ui', 'modals');
109
+ if (await fs.pathExists(modalsDir)) {
110
+ const modalFiles = await fs.readdir(modalsDir);
111
+ for (const file of modalFiles) {
112
+ if (file === '__pycache__' || file.endsWith('.pyc')) continue;
113
+ const className = file.replace('.py', '');
114
+ const displayName = UI_CLASS_MAP[className] || className;
115
+ result.hasModals.push(className);
116
+ result.workflows.push({
117
+ name: className,
118
+ display: displayName,
119
+ steps: [`Abrir ${displayName}`, 'Interactuar con datos', 'Confirmar o cancelar'],
120
+ });
121
+ }
122
+ }
123
+
124
+ // 5. Detectar features desde src/app.py
125
+ const appPy = path.join(projectDir, 'src', 'app.py');
126
+ if (await fs.pathExists(appPy)) {
127
+ const content = await fs.readFile(appPy, 'utf-8');
128
+
129
+ // Detectar auto-refresh
130
+ if (content.includes('auto_refresh') || content.includes('_auto_refresh')) {
131
+ result.hasAutoRefresh = true;
132
+ }
133
+ // Detectar busqueda
134
+ if (content.includes('search') || content.includes('SearchOverlay')) {
135
+ result.hasSearch = true;
136
+ }
137
+ // Detectar source1
138
+ if (content.includes('download_source1') || content.includes('source1')) {
139
+ result.hasSource1 = true;
140
+ }
141
+ // Detectar export
142
+ if (content.includes('export') || content.includes('excel') || content.includes('openpyxl')) {
143
+ result.hasExport = true;
144
+ }
145
+
146
+ // Extraer metodos _on_* como flujos de usuario
147
+ const methods = content.match(METHOD_PATTERN) || [];
148
+ for (const match of methods) {
149
+ const methodName = match.replace('def ', '').trim();
150
+ if (methodName.startsWith('_on_')) {
151
+ const displayName = methodName
152
+ .replace('_on_', '')
153
+ .replace(/_/g, ' ')
154
+ .replace(/\b\w/g, l => l.toUpperCase());
155
+ result.templates.push({
156
+ type: 'interaction',
157
+ name: methodName,
158
+ display: displayName,
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ // 6. Detectar Screenshots disponibles
165
+ const screenshotsDir = path.join(projectDir, 'assets', 'screenshots');
166
+ if (await fs.pathExists(screenshotsDir)) {
167
+ try {
168
+ const files = await fs.readdir(screenshotsDir);
169
+ result.screenshots = files.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f)).map(f => ({
170
+ filename: f,
171
+ path: path.join(screenshotsDir, f),
172
+ }));
173
+ } catch { /* ignorar */ }
174
+ }
175
+
176
+ // 7. Construir features list
177
+ result.features = result.features.map(f => ({
178
+ ...f,
179
+ screenshotIndex: result.screenshots.findIndex(s => s.filename.includes(f.name.toLowerCase().slice(0, 4))),
180
+ }));
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Mapeo de clase UI → tipo de slide recomendado.
187
+ */
188
+ export function classifyFeature(feature) {
189
+ if (feature.name.includes('Kpi') || feature.name.includes('Card')) return 'kpi';
190
+ if (feature.name.includes('Modal')) return 'workflow';
191
+ if (feature.name.includes('Dashboard')) return 'screenshot';
192
+ if (feature.name.includes('Search')) return 'feature';
193
+ return 'feature';
194
+ }