g360-cli 1.16.0 → 1.17.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 +37 -27
- package/package.json +1 -1
- package/src/assets/pptx/design-tokens.js +124 -0
- package/src/assets/pptx/layouts/base.js +357 -155
- package/src/assets/pptx/layouts/index.js +6 -1
- package/src/assets/pptx/scripts/analyze-app.js +177 -13
- package/src/assets/pptx/scripts/image-size.js +56 -0
- package/src/assets/pptx/templates/app-manual.js +324 -202
- package/src/assets/pptx/themes/base.js +6 -61
- package/src/assets/pptx/themes/cipsa.js +2 -44
- package/src/assets/pptx/themes/g360.js +2 -30
- package/src/assets/pptx/themes/index.js +87 -2
- package/src/commands/pptx.js +9 -7
|
@@ -89,18 +89,19 @@ export async function analyzeApp(projectDir) {
|
|
|
89
89
|
const uiFiles = await fs.readdir(uiDir);
|
|
90
90
|
for (const file of uiFiles) {
|
|
91
91
|
if (file === '__pycache__' || file.endsWith('.pyc')) continue;
|
|
92
|
+
if (file === '__init__.py') continue;
|
|
92
93
|
const filePath = path.join(uiDir, file);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
94
|
+
const st = await fs.stat(filePath).catch(() => null);
|
|
95
|
+
if (!st || !st.isFile()) continue;
|
|
96
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
97
|
+
const className = file.replace('.py', '');
|
|
98
|
+
const displayName = UI_CLASS_MAP[className] || className;
|
|
99
|
+
result.features.push({
|
|
100
|
+
name: className,
|
|
101
|
+
display: displayName,
|
|
102
|
+
file,
|
|
103
|
+
path: filePath,
|
|
104
|
+
});
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -110,6 +111,7 @@ export async function analyzeApp(projectDir) {
|
|
|
110
111
|
const modalFiles = await fs.readdir(modalsDir);
|
|
111
112
|
for (const file of modalFiles) {
|
|
112
113
|
if (file === '__pycache__' || file.endsWith('.pyc')) continue;
|
|
114
|
+
if (file === '__init__.py') continue;
|
|
113
115
|
const className = file.replace('.py', '');
|
|
114
116
|
const displayName = UI_CLASS_MAP[className] || className;
|
|
115
117
|
result.hasModals.push(className);
|
|
@@ -161,7 +163,10 @@ export async function analyzeApp(projectDir) {
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
164
|
-
// 6.
|
|
166
|
+
// 6. Detección web: framework, módulos y capacidades desde package.json
|
|
167
|
+
await analyzeWebApp(projectDir, result);
|
|
168
|
+
|
|
169
|
+
// 7. Detectar Screenshots disponibles
|
|
165
170
|
const screenshotsDir = path.join(projectDir, 'assets', 'screenshots');
|
|
166
171
|
if (await fs.pathExists(screenshotsDir)) {
|
|
167
172
|
try {
|
|
@@ -173,7 +178,7 @@ export async function analyzeApp(projectDir) {
|
|
|
173
178
|
} catch { /* ignorar */ }
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
//
|
|
181
|
+
// 8. Construir features list
|
|
177
182
|
result.features = result.features.map(f => ({
|
|
178
183
|
...f,
|
|
179
184
|
screenshotIndex: result.screenshots.findIndex(s => s.filename.includes(f.name.toLowerCase().slice(0, 4))),
|
|
@@ -182,6 +187,165 @@ export async function analyzeApp(projectDir) {
|
|
|
182
187
|
return result;
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Descripciones legibles para rutas web conocidas.
|
|
192
|
+
*/
|
|
193
|
+
const WEB_ROUTE_MAP = {
|
|
194
|
+
dashboard: 'Vista principal: resumen operativo, KPIs y accesos a los módulos',
|
|
195
|
+
hoy: 'Agenda del día: prioridades, alertas y avance',
|
|
196
|
+
radar: 'Oportunidades priorizadas y ruta del día',
|
|
197
|
+
netos: 'Montos netos jerárquicos por periodo',
|
|
198
|
+
clientes: 'Directorio de clientes con filtros y búsqueda',
|
|
199
|
+
ficha: 'Ficha de detalle por cliente con historial y precios',
|
|
200
|
+
login: 'Autenticación y validación de acceso',
|
|
201
|
+
reportes: 'Reportes y exportación de datos',
|
|
202
|
+
stock: 'Consulta de existencias en tiempo real',
|
|
203
|
+
pedidos: 'Gestión de pedidos y vigencias',
|
|
204
|
+
config: 'Configuración y preferencias de la app',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const WEB_COMPONENT_MAP = {
|
|
208
|
+
'app-root': 'Contenedor principal de la aplicación',
|
|
209
|
+
'stock-header': 'Encabezado con estado de conexión y acciones',
|
|
210
|
+
'stock-search': 'Búsqueda de productos con coincidencias en vivo',
|
|
211
|
+
'stock-alerts': 'Alertas de quiebres de stock y reposición',
|
|
212
|
+
'estado-panel': 'Panel de estado por almacén/producto',
|
|
213
|
+
'pulso-form': 'Formulario de registro de datos de campo',
|
|
214
|
+
'sin-catalogo-panel': 'Aviso de catálogo no disponible con acción de carga',
|
|
215
|
+
'login': 'Autenticación y validación de acceso',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Detección para apps web: SvelteKit (routes), React (pages),
|
|
220
|
+
* Lit/Componentes (src/components) y capacidades desde package.json.
|
|
221
|
+
*/
|
|
222
|
+
async function analyzeWebApp(projectDir, result) {
|
|
223
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
224
|
+
const pkg = (await fs.pathExists(pkgPath))
|
|
225
|
+
? await fs.readJson(pkgPath).catch(() => null)
|
|
226
|
+
: null;
|
|
227
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
228
|
+
const hasDep = (n) => Object.keys(deps).some((d) => d === n || d.startsWith(n));
|
|
229
|
+
|
|
230
|
+
if (pkg) {
|
|
231
|
+
result.name = result.name || pkg.name || '';
|
|
232
|
+
result.version = result.version || pkg.version || '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Framework
|
|
236
|
+
if (hasDep('@sveltejs/kit')) result.framework = result.framework || 'SvelteKit';
|
|
237
|
+
else if (hasDep('lit')) result.framework = result.framework || 'Lit';
|
|
238
|
+
else if (hasDep('react')) result.framework = result.framework || 'React';
|
|
239
|
+
else if (hasDep('vue')) result.framework = result.framework || 'Vue';
|
|
240
|
+
if (result.framework) result.type = 'web';
|
|
241
|
+
|
|
242
|
+
// Capabilidades desde dependencias
|
|
243
|
+
if (hasDep('exceljs') || hasDep('xlsx') || hasDep('sheetjs')) result.hasExport = true;
|
|
244
|
+
if (hasDep('@supabase/supabase-js')) result.hasSupabase = true;
|
|
245
|
+
if (hasDep('vite-plugin-pwa') || hasDep('workbox-window')) result.hasPwa = true;
|
|
246
|
+
if (hasDep('chart.js') || hasDep('recharts') || hasDep('echarts')) result.hasCharts = true;
|
|
247
|
+
if (hasDep('tailwindcss') || hasDep('@tailwindcss/vite')) result.hasTailwind = true;
|
|
248
|
+
|
|
249
|
+
// Módulos: SvelteKit routes (src/routes/**/+page.svelte, incluye subrutas dinámicas)
|
|
250
|
+
const routesDir = path.join(projectDir, 'src', 'routes');
|
|
251
|
+
if (await fs.pathExists(routesDir)) {
|
|
252
|
+
const entries = await fs.readdir(routesDir, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!entry.isDirectory()) continue;
|
|
255
|
+
const routeDir = path.join(routesDir, entry.name);
|
|
256
|
+
const pageFile = await findPageSvelte(routeDir);
|
|
257
|
+
if (!pageFile) continue;
|
|
258
|
+
result.features.push({
|
|
259
|
+
name: entry.name,
|
|
260
|
+
display: capitalize(entry.name),
|
|
261
|
+
desc: WEB_ROUTE_MAP[entry.name] || `Sección ${entry.name} de la aplicación.`,
|
|
262
|
+
file: path.relative(projectDir, pageFile).split(path.sep).join('/'),
|
|
263
|
+
path: pageFile,
|
|
264
|
+
kind: 'route',
|
|
265
|
+
});
|
|
266
|
+
result.modules.push(entry.name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Módulos: componentes web destacados (src/components/*.js) — solo si no hay routes
|
|
271
|
+
const componentsDir = path.join(projectDir, 'src', 'components');
|
|
272
|
+
if (result.features.length === 0 && await fs.pathExists(componentsDir)) {
|
|
273
|
+
const files = (await fs.readdir(componentsDir))
|
|
274
|
+
.filter((f) => /\.(js|ts)$/.test(f));
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
const base = file.replace(/\.(js|ts)$/, '');
|
|
277
|
+
result.features.push({
|
|
278
|
+
name: base,
|
|
279
|
+
display: componentDisplayName(base),
|
|
280
|
+
desc: WEB_COMPONENT_MAP[base] || `Componente de interfaz ${base}.`,
|
|
281
|
+
file: `src/components/${file}`,
|
|
282
|
+
path: path.join(componentsDir, file),
|
|
283
|
+
kind: 'component',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Buscador global: por nombre de componente/ruta o contenido de las páginas
|
|
289
|
+
if (result.features.some((f) => /search|busca/i.test(f.name))) {
|
|
290
|
+
result.hasSearch = true;
|
|
291
|
+
} else {
|
|
292
|
+
for (const f of result.features) {
|
|
293
|
+
if (f.path && /\.(svelte|js|ts)$/.test(f.path)) {
|
|
294
|
+
try {
|
|
295
|
+
const content = await fs.readFile(f.path, 'utf-8');
|
|
296
|
+
if (/search|buscador|lupa/i.test(content)) { result.hasSearch = true; break; }
|
|
297
|
+
} catch { /* ignorar */ }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Flujos web: modales/buscador como workflows si hay PWA/Supabase (solo títulos)
|
|
303
|
+
if (result.features.length > 0 && result.hasPwa) {
|
|
304
|
+
result.workflows.push({
|
|
305
|
+
name: 'PWA',
|
|
306
|
+
display: 'Instalación PWA y modo offline',
|
|
307
|
+
steps: [
|
|
308
|
+
'Abrir la URL de la app en el navegador',
|
|
309
|
+
'Instalar desde el menú (Android/iOS)',
|
|
310
|
+
'Usar con cache offline cuando no haya señal',
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (result.hasSearch) {
|
|
315
|
+
result.workflows.push({
|
|
316
|
+
name: 'Busqueda',
|
|
317
|
+
display: 'Búsqueda global',
|
|
318
|
+
steps: ['Abrir el buscador', 'Escribir el criterio', 'Abrir el resultado'],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function componentDisplayName(base) {
|
|
324
|
+
return base
|
|
325
|
+
.replace(/-([a-z])/g, (_, c) => ' ' + c.toUpperCase())
|
|
326
|
+
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function capitalize(s) {
|
|
330
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Busca recursivamente +page.svelte dentro de un directorio de ruta (subrutas dinámicas incluidas). */
|
|
334
|
+
async function findPageSvelte(dir, depth = 0) {
|
|
335
|
+
if (depth > 3) return null;
|
|
336
|
+
const direct = path.join(dir, '+page.svelte');
|
|
337
|
+
if (await fs.pathExists(direct)) return direct;
|
|
338
|
+
try {
|
|
339
|
+
const children = await fs.readdir(dir, { withFileTypes: true });
|
|
340
|
+
for (const child of children) {
|
|
341
|
+
if (!child.isDirectory()) continue;
|
|
342
|
+
const found = await findPageSvelte(path.join(dir, child.name), depth + 1);
|
|
343
|
+
if (found) return found;
|
|
344
|
+
}
|
|
345
|
+
} catch { /* ignorar */ }
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
185
349
|
/**
|
|
186
350
|
* Mapeo de clase UI → tipo de slide recomendado.
|
|
187
351
|
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lector de dimensiones de imagen sin dependencias externas.
|
|
3
|
+
* Soporta PNG y JPEG (los formatos aceptados por pptxgenjs).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Devuelve { width, height } leyendo los headers del archivo.
|
|
9
|
+
* @param {string} filePath
|
|
10
|
+
* @returns {{width:number,height:number}|null}
|
|
11
|
+
*/
|
|
12
|
+
export function getImageSize(filePath) {
|
|
13
|
+
let buf;
|
|
14
|
+
try {
|
|
15
|
+
buf = fs.readFileSync(filePath);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!buf || buf.length < 24) return null;
|
|
20
|
+
|
|
21
|
+
// PNG: firma 8 bytes + IHDR (largo 4 + tipo 4) -> ancho en 16, alto en 20 (BE)
|
|
22
|
+
if (buf.toString('ascii', 1, 4) === 'PNG') {
|
|
23
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// JPEG: recorrer marcadores hasta SOF0-2 (0xC0/0xC1/0xC2)
|
|
27
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
28
|
+
let off = 2;
|
|
29
|
+
while (off + 9 < buf.length) {
|
|
30
|
+
if (buf[off] !== 0xff) { off += 1; continue; }
|
|
31
|
+
const marker = buf[off + 1];
|
|
32
|
+
const isSOF = marker >= 0xc0 && marker <= 0xcf
|
|
33
|
+
&& marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
34
|
+
if (isSOF) {
|
|
35
|
+
return {
|
|
36
|
+
height: buf.readUInt16BE(off + 5),
|
|
37
|
+
width: buf.readUInt16BE(off + 7),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
off += 2 + buf.readUInt16BE(off + 2);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calcula encaje proporcional (contain) de la imagen en el marco.
|
|
48
|
+
* @returns {{dw:number,dh:number}}
|
|
49
|
+
*/
|
|
50
|
+
export function containFit(width, height, frameW, frameH) {
|
|
51
|
+
if (!width || !height) return { dw: frameW, dh: frameH };
|
|
52
|
+
const ir = width / height;
|
|
53
|
+
const fr = frameW / frameH;
|
|
54
|
+
if (ir > fr) return { dw: frameW, dh: frameW / ir };
|
|
55
|
+
return { dw: frameH * ir, dh: frameH };
|
|
56
|
+
}
|