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.
@@ -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
- 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
- }
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. Detectar Screenshots disponibles
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
- // 7. Construir features list
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
+ }