g360-cli 1.15.8 → 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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Layouts re-exportados desde base.js para compatibilidad.
3
+ */
4
+ export {
5
+ coverSlide,
6
+ screenshotSlide,
7
+ screenshotFrame,
8
+ featureSlide,
9
+ workflowSlide,
10
+ architectureSlide,
11
+ checklistSlide,
12
+ kpiSlide,
13
+ limitsSlide,
14
+ modulesTable,
15
+ card,
16
+ textBlock,
17
+ addFooter,
18
+ addSectionHeader,
19
+ } from './base.js';
@@ -0,0 +1,358 @@
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
+ if (file === '__init__.py') continue;
93
+ const filePath = path.join(uiDir, file);
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
+ });
105
+ }
106
+ }
107
+
108
+ // 4. Escanear modals
109
+ const modalsDir = path.join(projectDir, 'src', 'ui', 'modals');
110
+ if (await fs.pathExists(modalsDir)) {
111
+ const modalFiles = await fs.readdir(modalsDir);
112
+ for (const file of modalFiles) {
113
+ if (file === '__pycache__' || file.endsWith('.pyc')) continue;
114
+ if (file === '__init__.py') continue;
115
+ const className = file.replace('.py', '');
116
+ const displayName = UI_CLASS_MAP[className] || className;
117
+ result.hasModals.push(className);
118
+ result.workflows.push({
119
+ name: className,
120
+ display: displayName,
121
+ steps: [`Abrir ${displayName}`, 'Interactuar con datos', 'Confirmar o cancelar'],
122
+ });
123
+ }
124
+ }
125
+
126
+ // 5. Detectar features desde src/app.py
127
+ const appPy = path.join(projectDir, 'src', 'app.py');
128
+ if (await fs.pathExists(appPy)) {
129
+ const content = await fs.readFile(appPy, 'utf-8');
130
+
131
+ // Detectar auto-refresh
132
+ if (content.includes('auto_refresh') || content.includes('_auto_refresh')) {
133
+ result.hasAutoRefresh = true;
134
+ }
135
+ // Detectar busqueda
136
+ if (content.includes('search') || content.includes('SearchOverlay')) {
137
+ result.hasSearch = true;
138
+ }
139
+ // Detectar source1
140
+ if (content.includes('download_source1') || content.includes('source1')) {
141
+ result.hasSource1 = true;
142
+ }
143
+ // Detectar export
144
+ if (content.includes('export') || content.includes('excel') || content.includes('openpyxl')) {
145
+ result.hasExport = true;
146
+ }
147
+
148
+ // Extraer metodos _on_* como flujos de usuario
149
+ const methods = content.match(METHOD_PATTERN) || [];
150
+ for (const match of methods) {
151
+ const methodName = match.replace('def ', '').trim();
152
+ if (methodName.startsWith('_on_')) {
153
+ const displayName = methodName
154
+ .replace('_on_', '')
155
+ .replace(/_/g, ' ')
156
+ .replace(/\b\w/g, l => l.toUpperCase());
157
+ result.templates.push({
158
+ type: 'interaction',
159
+ name: methodName,
160
+ display: displayName,
161
+ });
162
+ }
163
+ }
164
+ }
165
+
166
+ // 6. Detección web: framework, módulos y capacidades desde package.json
167
+ await analyzeWebApp(projectDir, result);
168
+
169
+ // 7. Detectar Screenshots disponibles
170
+ const screenshotsDir = path.join(projectDir, 'assets', 'screenshots');
171
+ if (await fs.pathExists(screenshotsDir)) {
172
+ try {
173
+ const files = await fs.readdir(screenshotsDir);
174
+ result.screenshots = files.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f)).map(f => ({
175
+ filename: f,
176
+ path: path.join(screenshotsDir, f),
177
+ }));
178
+ } catch { /* ignorar */ }
179
+ }
180
+
181
+ // 8. Construir features list
182
+ result.features = result.features.map(f => ({
183
+ ...f,
184
+ screenshotIndex: result.screenshots.findIndex(s => s.filename.includes(f.name.toLowerCase().slice(0, 4))),
185
+ }));
186
+
187
+ return result;
188
+ }
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
+
349
+ /**
350
+ * Mapeo de clase UI → tipo de slide recomendado.
351
+ */
352
+ export function classifyFeature(feature) {
353
+ if (feature.name.includes('Kpi') || feature.name.includes('Card')) return 'kpi';
354
+ if (feature.name.includes('Modal')) return 'workflow';
355
+ if (feature.name.includes('Dashboard')) return 'screenshot';
356
+ if (feature.name.includes('Search')) return 'feature';
357
+ return 'feature';
358
+ }
@@ -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
+ }