g360-cli 1.15.8 → 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/README.md +72 -2
- package/package.json +2 -1
- package/src/assets/pptx/layouts/base.js +239 -0
- package/src/assets/pptx/layouts/index.js +14 -0
- package/src/assets/pptx/scripts/analyze-app.js +194 -0
- package/src/assets/pptx/templates/app-manual.js +275 -0
- package/src/assets/pptx/themes/base.js +63 -0
- package/src/assets/pptx/themes/cipsa.js +46 -0
- package/src/assets/pptx/themes/g360.js +32 -0
- package/src/assets/pptx/themes/index.js +2 -0
- package/src/cli.js +11 -0
- package/src/commands/pptx.js +89 -0
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ CLI tool para el ecosistema G360 que permite inicializar proyectos con estructur
|
|
|
97
97
|
|
|
98
98
|
## Versión
|
|
99
99
|
|
|
100
|
-
**Current: v1.
|
|
100
|
+
**Current: v1.16.0** — [Ver en npm](https://www.npmjs.com/package/g360-cli)
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
@@ -1337,7 +1337,77 @@ g360 lint --project ./mi-proyecto
|
|
|
1337
1337
|
|
|
1338
1338
|
---
|
|
1339
1339
|
|
|
1340
|
-
|
|
1340
|
+
### `g360 pptx`
|
|
1341
|
+
|
|
1342
|
+
Genera manuales y presentaciones en PowerPoint (.pptx) desde una app G360.
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
g360 pptx [ruta] [opciones]
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
**Opciones:**
|
|
1349
|
+
|
|
1350
|
+
| Opción | Descripción | Valor por defecto |
|
|
1351
|
+
|--------|-------------|-------------------|
|
|
1352
|
+
| `--mode <tipo>` | Modo de generacion (`manual`, `demo`, `onboarding`) | `manual` |
|
|
1353
|
+
| `--theme <nombre>` | Tema de marca (`g360`, `cipsa`) | auto-detected desde `skill.json` |
|
|
1354
|
+
| `--out <archivo>` | Ruta del archivo de salida | `{app-name}-manual.pptx` |
|
|
1355
|
+
| `--dry-run` | Solo muestra outline sin generar | `false` |
|
|
1356
|
+
|
|
1357
|
+
**Modos:**
|
|
1358
|
+
|
|
1359
|
+
| Modo | Proposito | Formato |
|
|
1360
|
+
|------|-----------|---------|
|
|
1361
|
+
| `manual` | Documentacion de usuario (A4 portrait) | Impresion/pdfs |
|
|
1362
|
+
| `demo` | Presentaciones comerciales (16:9) | Proyector/screens |
|
|
1363
|
+
| `onboarding` | Guia de inicio rapido | Pantalla |
|
|
1364
|
+
|
|
1365
|
+
**Ejemplos:**
|
|
1366
|
+
|
|
1367
|
+
```bash
|
|
1368
|
+
# Generar manual A4 para app CIPSA (auto-detected desde skill.json)
|
|
1369
|
+
g360 pptx ../mi-app --mode manual
|
|
1370
|
+
|
|
1371
|
+
# Solo preview del outline
|
|
1372
|
+
g360 pptx . --dry-run
|
|
1373
|
+
|
|
1374
|
+
# Presentacion 16:9 con tema G360
|
|
1375
|
+
g360 pptx . --mode demo --theme g360 --out demo.pptx
|
|
1376
|
+
|
|
1377
|
+
# Onboarding en modo print
|
|
1378
|
+
g360 pptx . --mode onboarding --out onboarding.pptx
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
**Estructura generada (modo manual, A4):**
|
|
1382
|
+
|
|
1383
|
+
| # | Slide | Contenido |
|
|
1384
|
+
|---|-------|-----------|
|
|
1385
|
+
| 1 | Portada | Nombre + descripcion + version |
|
|
1386
|
+
| 2 | ¿Que es? | Proposito de la app |
|
|
1387
|
+
| 3 | Instalacion | Pasos de inicio |
|
|
1388
|
+
| 4 | Dashboard | Screenshot principal (placeholder si no hay) |
|
|
1389
|
+
| 5-N | Features | Una por modulo UI detectado |
|
|
1390
|
+
| N+1 | Flujos | Modals y workflows |
|
|
1391
|
+
| N+2 | Arquitectura | Diagrama por capas |
|
|
1392
|
+
| N+3 | Buenas practicas | Checklist de uso |
|
|
1393
|
+
| N+4 | Resumen | Sintesis final |
|
|
1394
|
+
|
|
1395
|
+
**Screenshots:**
|
|
1396
|
+
|
|
1397
|
+
Colocar imagenes en `assets/screenshots/` para inclusion automatica:
|
|
1398
|
+
- `dashboard.png` — vista principal
|
|
1399
|
+
- `kpi-card.png`, `modal-export.png` — componentes relevantes
|
|
1400
|
+
|
|
1401
|
+
Si no hay screenshots, se generan placeholders con marco punteado.
|
|
1402
|
+
|
|
1403
|
+
**Temas disponibles:**
|
|
1404
|
+
|
|
1405
|
+
- **`g360`**: Esmeralda #10B981, fondo claro
|
|
1406
|
+
- **`cipsa`**: Verde CIPSA #00d084, logo corporativo incluido
|
|
1407
|
+
|
|
1408
|
+
---
|
|
1409
|
+
|
|
1410
|
+
## Integracion con OpenCode
|
|
1341
1411
|
|
|
1342
1412
|
g360-cli incluye integración con **OpenCode** para desarrollo asistido por IA. Esta integración permite que los agentes de IA tengan acceso a los recursos de g360-cli durante el desarrollo.
|
|
1343
1413
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "g360-cli",
|
|
3
|
-
"version": "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
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Manual de App G360 — 12 slides A4.
|
|
3
|
+
*
|
|
4
|
+
* Estructura:
|
|
5
|
+
* 1. Portada
|
|
6
|
+
* 2. ¿Qué es esta app?
|
|
7
|
+
* 3. Requisitos / Instalacion
|
|
8
|
+
* 4. Inicio — primera pantalla
|
|
9
|
+
* 5-N. Funcionalidades (una por feature detectada)
|
|
10
|
+
* N+1. Flujos de trabajo (modals)
|
|
11
|
+
* N+2. Arquitectura
|
|
12
|
+
* N+3. Buenas prácticas
|
|
13
|
+
* N+4. Resumen
|
|
14
|
+
*/
|
|
15
|
+
import PptxGenJS from 'pptxgenjs';
|
|
16
|
+
import { createG360Theme, createCipsaTheme } from '../themes/index.js';
|
|
17
|
+
import {
|
|
18
|
+
coverSlide,
|
|
19
|
+
screenshotSlide,
|
|
20
|
+
featureSlide,
|
|
21
|
+
workflowSlide,
|
|
22
|
+
architectureSlide,
|
|
23
|
+
checklistSlide,
|
|
24
|
+
kpiSlide,
|
|
25
|
+
addFooter,
|
|
26
|
+
addSectionHeader,
|
|
27
|
+
} from '../layouts/base.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Genera un manual PPTX completo para una app G360.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} appData — resultado de analyzeApp()
|
|
33
|
+
* @param {object} options — { mode, theme, outDir }
|
|
34
|
+
* @returns {Promise<string>} — ruta del archivo .pptx generado
|
|
35
|
+
*/
|
|
36
|
+
export async function generateManualPptx(appData, options = {}) {
|
|
37
|
+
const { mode = 'manual', theme: themeName = null } = options;
|
|
38
|
+
|
|
39
|
+
// Seleccionar theme
|
|
40
|
+
const theme = themeName === 'cipsa' || (!themeName && appData.brand === 'cipsa')
|
|
41
|
+
? createCipsaTheme()
|
|
42
|
+
: createG360Theme();
|
|
43
|
+
|
|
44
|
+
const { pptx, colors } = theme;
|
|
45
|
+
|
|
46
|
+
// Configurar tamaño segun modo
|
|
47
|
+
if (mode === 'demo') {
|
|
48
|
+
pptx.layout = 'LAYOUT_16x9';
|
|
49
|
+
} else {
|
|
50
|
+
pptx.layout = 'A4';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ===== SLIDE 1: Portada =====
|
|
54
|
+
let slide = pptx.addSlide();
|
|
55
|
+
slide.background = { color: colors.bg };
|
|
56
|
+
coverSlide(slide, {
|
|
57
|
+
appName: appData.name || 'Mi Aplicacion G360',
|
|
58
|
+
description: appData.description || 'Documentacion de uso y funcionalidades',
|
|
59
|
+
version: appData.version,
|
|
60
|
+
brand: appData.brand,
|
|
61
|
+
}, theme);
|
|
62
|
+
addFooter(slide, theme, 1, 12);
|
|
63
|
+
|
|
64
|
+
// ===== SLIDE 2: ¿Qué es? =====
|
|
65
|
+
slide = pptx.addSlide();
|
|
66
|
+
slide.background = { color: colors.bg };
|
|
67
|
+
addSectionHeader(slide, 'INTRODUCCION', theme);
|
|
68
|
+
featureSlide(slide, {
|
|
69
|
+
title: '¿Qué es ' + (appData.name || 'esta app') + '?',
|
|
70
|
+
description: appData.description || 'Aplicacion de escritorio construida con Flet y los estandares G360.',
|
|
71
|
+
bullets: [
|
|
72
|
+
'Monitoriza datos en tiempo real desde el ERP',
|
|
73
|
+
'Genera reportes y analisis automaticos',
|
|
74
|
+
'Soporta dual theme (claro/oscuro)',
|
|
75
|
+
'Funciona offline con cache local',
|
|
76
|
+
],
|
|
77
|
+
}, theme);
|
|
78
|
+
addFooter(slide, theme, 2, 12);
|
|
79
|
+
|
|
80
|
+
// ===== SLIDE 3: Instalacion =====
|
|
81
|
+
slide = pptx.addSlide();
|
|
82
|
+
slide.background = { color: colors.bg };
|
|
83
|
+
addSectionHeader(slide, 'INSTALACION', theme);
|
|
84
|
+
const installSteps = [
|
|
85
|
+
{ title: 'Ejecutar run.bat', desc: 'El launcher instala uv, Python 3.11, dependencias y crea acceso directo.' },
|
|
86
|
+
{ title: 'O usar comando directo', desc: 'uv sync && uv run python main.py' },
|
|
87
|
+
{ title: 'Version portable', desc: 'Descomprimir zip y ejecutar launch.vbs (sin dependencias).' },
|
|
88
|
+
];
|
|
89
|
+
workflowSlide(slide, {
|
|
90
|
+
title: 'Como iniciar la aplicacion',
|
|
91
|
+
steps: installSteps,
|
|
92
|
+
}, theme);
|
|
93
|
+
addFooter(slide, theme, 3, 12);
|
|
94
|
+
|
|
95
|
+
// ===== SLIDES 4+: Features / Screenshots =====
|
|
96
|
+
let slideNum = 4;
|
|
97
|
+
const totalFeatures = Math.min(appData.features.length, 6); // max 6 features en manual
|
|
98
|
+
const totalSlides = 3 + totalFeatures + 3; // intro + features + workflows + arch + summary
|
|
99
|
+
|
|
100
|
+
// Slide 4: Dashboard / inicio
|
|
101
|
+
slide = pptx.addSlide();
|
|
102
|
+
slide.background = { color: colors.bg };
|
|
103
|
+
addSectionHeader(slide, 'PRIMERA VISTA', theme);
|
|
104
|
+
const dashScreen = appData.screenshots?.find(s => s.filename.toLowerCase().includes('dash'));
|
|
105
|
+
screenshotSlide(slide, {
|
|
106
|
+
title: 'Dashboard Principal',
|
|
107
|
+
description: appData.hasAutoRefresh
|
|
108
|
+
? 'Vista principal con KPIs en tiempo real y auto-refresh cada 15 minutos.'
|
|
109
|
+
: 'Vista principal de la aplicacion con los indicadores clave del negocio.',
|
|
110
|
+
screenshotPath: dashScreen?.path,
|
|
111
|
+
}, theme);
|
|
112
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
113
|
+
slideNum++;
|
|
114
|
+
|
|
115
|
+
// Features individual slides
|
|
116
|
+
for (const feature of appData.features.slice(0, totalFeatures)) {
|
|
117
|
+
slide = pptx.addSlide();
|
|
118
|
+
slide.background = { color: colors.bg };
|
|
119
|
+
addSectionHeader(slide, 'FUNCIONALIDAD', theme);
|
|
120
|
+
|
|
121
|
+
const shot = appData.screenshots?.find(s =>
|
|
122
|
+
feature.name.toLowerCase().includes(s.filename.toLowerCase().slice(0, 6))
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const classification = classifyFeatureForSlide(feature);
|
|
126
|
+
|
|
127
|
+
if (classification === 'kpi' && feature.name.includes('Kpi')) {
|
|
128
|
+
kpiSlide(slide, {
|
|
129
|
+
title: feature.display || feature.name,
|
|
130
|
+
kpis: generateKpiPlaceholders(feature),
|
|
131
|
+
}, theme);
|
|
132
|
+
} else if (classification === 'workflow') {
|
|
133
|
+
workflowSlide(slide, {
|
|
134
|
+
title: feature.display || feature.name,
|
|
135
|
+
steps: [`Acceder desde el menu principal`, 'Seleccionar opciones disponibles', 'Confirmar y generar resultado'],
|
|
136
|
+
}, theme);
|
|
137
|
+
} else {
|
|
138
|
+
featureSlide(slide, {
|
|
139
|
+
title: feature.display || feature.name,
|
|
140
|
+
description: `Module ubicado en src/ui/${feature.file}`,
|
|
141
|
+
bullets: [
|
|
142
|
+
'Clase exportada desde src/ui/',
|
|
143
|
+
'Integracion con Dashboard principal',
|
|
144
|
+
'Soporta dual theme automaticamente',
|
|
145
|
+
],
|
|
146
|
+
}, theme);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!shot) {
|
|
150
|
+
// Agregar placeholder si no hay screenshot
|
|
151
|
+
slide.addShape('rect', [1, 3.5, 8, 6], {
|
|
152
|
+
fill: { color: colors.surface },
|
|
153
|
+
line: { color: colors.border, width: 1, dashType: 'dash' },
|
|
154
|
+
});
|
|
155
|
+
slide.addText('📷 Screenshot: ' + (feature.display || feature.name), [1, 6, 8, 0.5], {
|
|
156
|
+
fontSize: 12, color: colors.textLight, fontFace: 'Inter', align: 'center',
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
screenshotSlide(slide, {
|
|
160
|
+
title: feature.display || feature.name,
|
|
161
|
+
description: `Módulo: src/ui/${feature.file}`,
|
|
162
|
+
screenshotPath: shot.path,
|
|
163
|
+
}, theme);
|
|
164
|
+
slide.clearShapes?.(); // remove placeholder
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
168
|
+
slideNum++;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ===== Workflows (modals) =====
|
|
172
|
+
if (appData.hasModals.length > 0) {
|
|
173
|
+
slide = pptx.addSlide();
|
|
174
|
+
slide.background = { color: colors.bg };
|
|
175
|
+
addSectionHeader(slide, 'FLUJOS DE TRABAJO', theme);
|
|
176
|
+
|
|
177
|
+
const steps = appData.hasModals.slice(0, 4).map((modal, i) => ({
|
|
178
|
+
title: `Modal: ${modal}`,
|
|
179
|
+
desc: 'Paso 1 → Paso 2 → Paso 3',
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
workflowSlide(slide, {
|
|
183
|
+
title: 'Modales y flujos de usuario',
|
|
184
|
+
steps,
|
|
185
|
+
}, theme);
|
|
186
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
187
|
+
slideNum++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ===== Architecture =====
|
|
191
|
+
slide = pptx.addSlide();
|
|
192
|
+
slide.background = { color: colors.bg };
|
|
193
|
+
addSectionHeader(slide, 'ARQUITECTURA', theme);
|
|
194
|
+
architectureSlide(slide, {
|
|
195
|
+
title: 'Estructura de capas',
|
|
196
|
+
layers: [
|
|
197
|
+
{ name: 'UI', desc: 'Dashboard, Cards, Modals, Overlays', color: colors.accent },
|
|
198
|
+
{ name: 'Core', desc: 'Processor, Downloader, Models', color: colors.info },
|
|
199
|
+
{ name: 'Config', desc: 'Theme, Constants, Skill', color: colors.violet },
|
|
200
|
+
{ name: 'Data', desc: 'API S1, Catalogo JSON, Cache', color: colors.success },
|
|
201
|
+
],
|
|
202
|
+
}, theme);
|
|
203
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
204
|
+
slideNum++;
|
|
205
|
+
|
|
206
|
+
// ===== Checklist / Buenas prácticas =====
|
|
207
|
+
slide = pptx.addSlide();
|
|
208
|
+
slide.background = { color: colors.bg };
|
|
209
|
+
addSectionHeader(slide, 'BUENAS PRÁCTICAS', theme);
|
|
210
|
+
checklistSlide(slide, {
|
|
211
|
+
title: 'Recomendaciones de uso',
|
|
212
|
+
items: [
|
|
213
|
+
{ text: 'Mantener nombres de clases siguiendo PascalCase (KpiCard, Dashboard)', checked: true },
|
|
214
|
+
{ text: 'Usar theme.py para colores, nunca hardcodear #HEX', checked: true },
|
|
215
|
+
{ text: 'Registrar eventos con publish_g360_event() para comunicacion entre apps', checked: true },
|
|
216
|
+
{ text: 'Implementar shutdown() para limpieza de threads al cerrar', checked: true },
|
|
217
|
+
{ text: 'Documentar funciones con docstrings en español', checked: true },
|
|
218
|
+
{ text: 'Usar G360 Signature widget en el footer de la app', checked: true },
|
|
219
|
+
],
|
|
220
|
+
}, theme);
|
|
221
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
222
|
+
slideNum++;
|
|
223
|
+
|
|
224
|
+
// ===== Summary =====
|
|
225
|
+
slide = pptx.addSlide();
|
|
226
|
+
slide.background = { color: colors.bg };
|
|
227
|
+
addSectionHeader(slide, 'RESUMEN', theme);
|
|
228
|
+
|
|
229
|
+
const summaryItems = [
|
|
230
|
+
`Nombre: ${appData.name || 'N/A'}`,
|
|
231
|
+
`Version: ${appData.version || '1.0.0'}`,
|
|
232
|
+
`Framework: ${appData.framework || 'Flet'}`,
|
|
233
|
+
`Skills: ${appData.skill || 'N/A'}`,
|
|
234
|
+
`Caracteristicas: ${appData.features.length} modulos UI`,
|
|
235
|
+
appData.hasAutoRefresh ? 'Auto-refresh activado' : '',
|
|
236
|
+
appData.hasSearch ? 'Buscador flotante implementado' : '',
|
|
237
|
+
appData.hasExport ? 'Exportacion a Excel disponible' : '',
|
|
238
|
+
appData.hasModals.length > 0 ? `${appData.hasModals.length} modales de trabajo` : '',
|
|
239
|
+
].filter(Boolean);
|
|
240
|
+
|
|
241
|
+
checklistSlide(slide, {
|
|
242
|
+
title: 'Resumen de la aplicacion',
|
|
243
|
+
items: summaryItems.map(t => ({ text: t, checked: true })),
|
|
244
|
+
}, theme);
|
|
245
|
+
addFooter(slide, theme, slideNum, totalSlides);
|
|
246
|
+
|
|
247
|
+
// Save
|
|
248
|
+
const outPath = options.outPath || path.join(process.cwd(), `${(appData.name || 'app').toLowerCase().replace(/\s+/g, '-')}-manual.pptx`);
|
|
249
|
+
await pptx.writeFile({ fileName: outPath });
|
|
250
|
+
return outPath;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Classifica feature para decidir que slide usar.
|
|
255
|
+
*/
|
|
256
|
+
function classifyFeatureForSlide(feature) {
|
|
257
|
+
if (feature.name.includes('Kpi')) return 'kpi';
|
|
258
|
+
if (feature.name.includes('Modal') || feature.name.includes('Dialog')) return 'workflow';
|
|
259
|
+
if (feature.name.includes('Dashboard')) return 'screenshot';
|
|
260
|
+
return 'feature';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Genera placeholders de KPIs basado en features detectadas.
|
|
265
|
+
*/
|
|
266
|
+
function generateKpiPlaceholders(feature) {
|
|
267
|
+
return [
|
|
268
|
+
{ label: 'Total', value: '—', color: '#64748B' },
|
|
269
|
+
{ label: 'Con Stock', value: '—', color: '#34D399' },
|
|
270
|
+
{ label: 'Sin Stock', value: '—', color: '#EF4444' },
|
|
271
|
+
{ label: 'Alertas', value: '—', color: '#F59E0B' },
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
import path from 'path';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema G360 — paleta esmeralda, fuente Inter, fondo oscuro/claro.
|
|
3
|
+
* Usa los colores definidos en skill.json del proyecto (fallback a valores G360).
|
|
4
|
+
*/
|
|
5
|
+
import PptxGenJS from 'pptxgenjs';
|
|
6
|
+
|
|
7
|
+
export function createG360Theme() {
|
|
8
|
+
const colors = {
|
|
9
|
+
accent: '#10B981',
|
|
10
|
+
accentDark: '#047857',
|
|
11
|
+
success: '#34D399',
|
|
12
|
+
warning: '#F59E0B',
|
|
13
|
+
error: '#EF4444',
|
|
14
|
+
text: '#0F172A',
|
|
15
|
+
textLight: '#64748B',
|
|
16
|
+
bg: '#FFFFFF',
|
|
17
|
+
surface: '#F3F5F9',
|
|
18
|
+
border: '#E3E8F0',
|
|
19
|
+
darkBg: '#0A0F1E',
|
|
20
|
+
darkText: '#F1F5FB',
|
|
21
|
+
darkSurface: '#141D33',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Definir layout A4 vertical para manual (portrait)
|
|
25
|
+
const pptx = new PptxGenJS();
|
|
26
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 }); // A4 portrait en pulgadas
|
|
27
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
28
|
+
pptx.defineLayout({ name: 'LAYOUT_4x3', width: 10, height: 7.5 });
|
|
29
|
+
|
|
30
|
+
// Usar A4 como default (manual mode)
|
|
31
|
+
pptx.layout = 'A4';
|
|
32
|
+
|
|
33
|
+
// Theme global
|
|
34
|
+
pptx.author = 'g360-cli';
|
|
35
|
+
pptx.title = '';
|
|
36
|
+
pptx.subject = 'Documentacion generada por g360-cli';
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
pptx,
|
|
40
|
+
colors,
|
|
41
|
+
layout: 'A4',
|
|
42
|
+
fonts: ['Inter', 'Arial'],
|
|
43
|
+
/**
|
|
44
|
+
* Agregar logo de marca a la presentacion
|
|
45
|
+
*/
|
|
46
|
+
setLogo(pptxObj, brand, width = 1.2, height = 0.6) {
|
|
47
|
+
const logoPath = brand === 'cipsa'
|
|
48
|
+
? path.join(__dirname, '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.svg')
|
|
49
|
+
: path.join(__dirname, '..', 'brand', 'g360', 'logotypes', 'logo-g360-light.svg');
|
|
50
|
+
// Nota: SVG no soportado nativamente por PptxGenJS; fallback a PNG
|
|
51
|
+
const pngPath = brand === 'cipsa'
|
|
52
|
+
? path.join(__dirname, '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.png')
|
|
53
|
+
: path.join(__dirname, '..', 'brand', 'g360', 'logotypes', 'logo-g360-dark.png');
|
|
54
|
+
if (require('fs').existsSync(pngPath)) {
|
|
55
|
+
return { path: pngPath, width, height };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
import path from 'path';
|
|
63
|
+
import fs from 'fs';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema CIPSA — verde corporativo con logo CIPSA.
|
|
3
|
+
*/
|
|
4
|
+
import PptxGenJS from 'pptxgenjs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
export function createCipsaTheme() {
|
|
9
|
+
const colors = {
|
|
10
|
+
accent: '#00d084',
|
|
11
|
+
accentDark: '#00796B',
|
|
12
|
+
success: '#22c55e',
|
|
13
|
+
warning: '#f59e0b',
|
|
14
|
+
error: '#ef4444',
|
|
15
|
+
text: '#0F172A',
|
|
16
|
+
textLight: '#64748B',
|
|
17
|
+
bg: '#FFFFFF',
|
|
18
|
+
surface: '#F0F9F4',
|
|
19
|
+
border: '#CCF0E0',
|
|
20
|
+
darkBg: '#0d1117',
|
|
21
|
+
darkText: '#f0f6fc',
|
|
22
|
+
darkSurface: '#161b22',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const pptx = new PptxGenJS();
|
|
26
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 });
|
|
27
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
28
|
+
pptx.layout = 'A4';
|
|
29
|
+
|
|
30
|
+
pptx.author = 'g360-cli';
|
|
31
|
+
pptx.title = '';
|
|
32
|
+
pptx.subject = 'Documentacion CIPSA generada por g360-cli';
|
|
33
|
+
|
|
34
|
+
const cipsaLogo = path.join(process.cwd(), 'assets', 'images', 'Logo_cipsa_solid.png');
|
|
35
|
+
const fallbackLogo = fs.existsSync(cipsaLogo)
|
|
36
|
+
? cipsaLogo
|
|
37
|
+
: path.join(__dirname, '..', '..', 'brand', 'cipsa', 'logotypes', 'Logo_cipsa_solid.png');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
pptx,
|
|
41
|
+
colors,
|
|
42
|
+
layout: 'A4',
|
|
43
|
+
fonts: ['Inter', 'Arial'],
|
|
44
|
+
logo: fs.existsSync(fallbackLogo) ? { path: fallbackLogo, width: 1.5, height: 0.5 } : null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tema G360 — paleta esmeralda, fuente Inter.
|
|
3
|
+
*/
|
|
4
|
+
import PptxGenJS from 'pptxgenjs';
|
|
5
|
+
|
|
6
|
+
const colors = {
|
|
7
|
+
accent: '#10B981',
|
|
8
|
+
accentDark: '#047857',
|
|
9
|
+
success: '#34D399',
|
|
10
|
+
warning: '#F59E0B',
|
|
11
|
+
error: '#EF4444',
|
|
12
|
+
info: '#3B82F6',
|
|
13
|
+
violet: '#8B5CF6',
|
|
14
|
+
text: '#0F172A',
|
|
15
|
+
textLight: '#64748B',
|
|
16
|
+
bg: '#FFFFFF',
|
|
17
|
+
surface: '#F3F5F9',
|
|
18
|
+
border: '#E3E8F0',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function createG360Theme() {
|
|
22
|
+
const pptx = new PptxGenJS();
|
|
23
|
+
pptx.defineLayout({ name: 'A4', width: 10, height: 14.28 });
|
|
24
|
+
pptx.defineLayout({ name: 'LAYOUT_16x9', width: 10, height: 5.625 });
|
|
25
|
+
pptx.layout = 'A4';
|
|
26
|
+
pptx.author = 'g360-cli';
|
|
27
|
+
pptx.subject = 'Documentacion generada por g360-cli';
|
|
28
|
+
|
|
29
|
+
return { pptx, colors, layout: 'A4' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { colors };
|
package/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { ingest } from './commands/ingest.js';
|
|
|
22
22
|
import { addon } from './commands/addon.js';
|
|
23
23
|
import { docs } from './commands/docs.js';
|
|
24
24
|
import { lint } from './commands/lint.js';
|
|
25
|
+
import { pptx } from './commands/pptx.js';
|
|
25
26
|
|
|
26
27
|
// Comando config (no requiere archivo separado)
|
|
27
28
|
function configAction(options) {
|
|
@@ -212,4 +213,14 @@ program
|
|
|
212
213
|
.option('--project <path>', 'Project path', '.')
|
|
213
214
|
.action(lint);
|
|
214
215
|
|
|
216
|
+
program
|
|
217
|
+
.command('pptx')
|
|
218
|
+
.argument('[path]', 'Project path to generate presentation from', '.')
|
|
219
|
+
.option('-m, --mode <type>', 'Mode: manual, demo, onboarding', 'manual')
|
|
220
|
+
.option('-t, --theme <name>', 'Brand theme: g360, cipsa (auto-detected from skill.json)', null)
|
|
221
|
+
.option('-o, --out <file>', 'Output file path (default: {app-name}-manual.pptx)', null)
|
|
222
|
+
.option('--dry-run', 'Preview outline without generating')
|
|
223
|
+
.option('--screenshots <dir>', 'Custom screenshots directory', 'assets/screenshots')
|
|
224
|
+
.action(pptx);
|
|
225
|
+
|
|
215
226
|
program.parse();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comando: g360 pptx [path] [opciones]
|
|
3
|
+
* Genera presentacion/manual PPTX para una app G360.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
|
|
11
|
+
import { analyzeApp } from '../assets/pptx/scripts/analyze-app.js';
|
|
12
|
+
import { generateManualPptx } from '../assets/pptx/templates/app-manual.js';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
export async function pptx(targetPath, options) {
|
|
17
|
+
const {
|
|
18
|
+
mode = 'manual',
|
|
19
|
+
theme: themeName = null,
|
|
20
|
+
out,
|
|
21
|
+
dryRun = false,
|
|
22
|
+
screenshots,
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
const targetDir = path.join(process.cwd(), targetPath || '.');
|
|
26
|
+
|
|
27
|
+
if (!await fs.pathExists(targetDir)) {
|
|
28
|
+
console.error(chalk.red(`❌ Directorio no encontrado: ${targetDir}`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detectar branding desde skill.json
|
|
33
|
+
const skillPath = path.join(targetDir, 'skill.json');
|
|
34
|
+
let detectedBrand = 'g360';
|
|
35
|
+
if (await fs.pathExists(skillPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const skill = await fs.readJson(skillPath);
|
|
38
|
+
detectedBrand = skill.brand || 'g360';
|
|
39
|
+
if (detectedBrand === 'cipsa' && !themeName) themeName = 'cipsa';
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.bold.cyan('\n📊 G360 App → PPTX Generator\n'));
|
|
44
|
+
console.log(chalk.gray(` Proyecto: ${targetDir}`));
|
|
45
|
+
console.log(chalk.gray(` Modo: ${mode}`));
|
|
46
|
+
console.log(chalk.gray(` Theme: ${themeName || detectedBrand}`));
|
|
47
|
+
if (out) console.log(chalk.gray(` Salida: ${out}`));
|
|
48
|
+
console.log('');
|
|
49
|
+
|
|
50
|
+
if (dryRun) {
|
|
51
|
+
console.log(chalk.yellow('📋 DRY RUN — Outline generado:\n'));
|
|
52
|
+
console.log(chalk.gray(' [1] Portada'));
|
|
53
|
+
console.log(chalk.gray(' [2] Introducción'));
|
|
54
|
+
console.log(chalk.gray(' [3] Instalación'));
|
|
55
|
+
console.log(chalk.gray(' [4] Dashboard / Inicio'));
|
|
56
|
+
console.log(chalk.gray(' [5-N] Funcionalidades (una por módulo UI detectado)'));
|
|
57
|
+
console.log(chalk.gray(' [N+1] Flujos de trabajo (modales)'));
|
|
58
|
+
console.log(chalk.gray(' [N+2] Arquitectura'));
|
|
59
|
+
console.log(chalk.gray(' [N+3] Buenas prácticas'));
|
|
60
|
+
console.log(chalk.gray(' [N+4] Resumen'));
|
|
61
|
+
console.log(chalk.gray(`\nTotal estimado: ~${12 + Math.min(6, 20)} slides A4`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = ora('Analizando aplicación...').start();
|
|
66
|
+
try {
|
|
67
|
+
const appData = await analyzeApp(targetDir);
|
|
68
|
+
spinner.text = `Generando ${mode} (${appData.features.length} módulos detectados)...`;
|
|
69
|
+
|
|
70
|
+
const pptxPath = await generateManualPptx(appData, {
|
|
71
|
+
mode,
|
|
72
|
+
theme: themeName || detectedBrand,
|
|
73
|
+
outPath: out,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
spinner.succeed(chalk.green(`✅ PPTX generado: ${pptxPath}`));
|
|
77
|
+
console.log(chalk.gray(` Tamaño: ${Math.round((await fs.stat(pptxPath)).size / 1024)} KB`));
|
|
78
|
+
console.log(chalk.gray(` Slides: ~12 A4 (modo manual)`));
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log(chalk.cyan(' Siguiente paso:'));
|
|
81
|
+
console.log(chalk.gray(' Revisar el archivo y reemplazar placeholders de screenshots'));
|
|
82
|
+
console.log(chalk.gray(` con imágenes reales de la app en ${path.join(targetDir, 'assets', 'screenshots')}/`));
|
|
83
|
+
console.log('');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
spinner.fail(chalk.red(`❌ Error: ${err.message}`));
|
|
86
|
+
console.error(err.stack);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|