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/README.md +1547 -1424
- 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/assets/templates/python-flet-polished/README.md +39 -0
- package/src/assets/templates/python-flet-polished/main.py +7 -0
- package/src/assets/templates/python-flet-polished/pyproject.toml +1 -0
- package/src/assets/templates/python-flet-polished/requirements.txt +1 -0
- package/src/assets/templates/python-flet-polished/skill.json +27 -1
- package/src/assets/templates/python-flet-polished/src/app.py +44 -1
- package/src/assets/templates/python-flet-polished/src/config/theme.py +153 -74
- package/src/assets/templates/python-flet-polished/src/core/g360_registry.py +562 -0
- package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +2 -2
- package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +1 -1
- package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +4 -4
- package/src/cli.js +11 -0
- package/src/commands/addon.test.js +2 -2
- package/src/commands/pptx.js +89 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G360 App Registry — Sistema de identidad y descubrimiento entre apps G360.
|
|
3
|
+
|
|
4
|
+
Cada app G360 se registra automaticamente al iniciar con:
|
|
5
|
+
- Nombre unico
|
|
6
|
+
- Version
|
|
7
|
+
- Skill aplicado
|
|
8
|
+
- Eventos disponibles
|
|
9
|
+
- Endpoints de comunicacion
|
|
10
|
+
- Version del evento (para backward compatibility)
|
|
11
|
+
|
|
12
|
+
Las apps pueden descubrir otras apps y enviar eventos entre si.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import threading
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Callable, Any, Dict, List, Optional, Awaitable, Union
|
|
21
|
+
from dataclasses import dataclass, field, asdict
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from functools import wraps
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import asyncio
|
|
27
|
+
_has_asyncio = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
_has_asyncio = False
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("g360.registry")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Ruta del registry global
|
|
35
|
+
REGISTRY_FILE = Path.home() / ".g360" / "apps_registry.json"
|
|
36
|
+
|
|
37
|
+
# Version del schema de registry (para migration)
|
|
38
|
+
REGISTRY_SCHEMA_VERSION = "1.0.0"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class AppMetadata:
|
|
43
|
+
"""Metadatos de una app G360."""
|
|
44
|
+
name: str
|
|
45
|
+
version: str
|
|
46
|
+
skill: str
|
|
47
|
+
framework: str
|
|
48
|
+
description: str = ""
|
|
49
|
+
author: str = ""
|
|
50
|
+
created_at: str = ""
|
|
51
|
+
updated_at: str = ""
|
|
52
|
+
|
|
53
|
+
# Version del evento (para compatibilidad)
|
|
54
|
+
event_schema_version: str = "1.0.0"
|
|
55
|
+
|
|
56
|
+
# Eventos que esta app puede procesar
|
|
57
|
+
events: List[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
# Endpoints de comunicacion
|
|
60
|
+
endpoints: Dict[str, str] = field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
# Estado de salud
|
|
63
|
+
status: str = "offline" # online, offline, error, starting
|
|
64
|
+
last_seen: str = ""
|
|
65
|
+
uptime: float = 0.0
|
|
66
|
+
|
|
67
|
+
# Versiones de dependencias
|
|
68
|
+
dependencies: Dict[str, str] = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
return asdict(self)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: dict) -> 'AppMetadata':
|
|
75
|
+
# Fallback para campos faltantes (migration)
|
|
76
|
+
missing_fields = set(cls.__dataclass_fields__.keys()) - set(data.keys())
|
|
77
|
+
for field_name in missing_fields:
|
|
78
|
+
default = cls.__dataclass_fields__[field_name].default
|
|
79
|
+
if callable(default):
|
|
80
|
+
data[field_name] = default()
|
|
81
|
+
else:
|
|
82
|
+
data[field_name] = default
|
|
83
|
+
return cls(**data)
|
|
84
|
+
|
|
85
|
+
def is_recent(self, max_age_seconds: int = 300) -> bool:
|
|
86
|
+
"""Verificar si la app fue vista recientemente."""
|
|
87
|
+
if not self.last_seen:
|
|
88
|
+
return False
|
|
89
|
+
try:
|
|
90
|
+
last_seen = datetime.fromisoformat(self.last_seen)
|
|
91
|
+
age = (datetime.now() - last_seen).total_seconds()
|
|
92
|
+
return age < max_age_seconds
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class EventCallback:
|
|
98
|
+
"""Wrapper para callbacks que soporta sync y async."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, callback: Callable, is_async: bool = False):
|
|
101
|
+
self.callback = callback
|
|
102
|
+
self.is_async = is_async
|
|
103
|
+
|
|
104
|
+
async def __call__(self, data: Optional[dict] = None):
|
|
105
|
+
"""Ejecutar el callback, manejando sync/async."""
|
|
106
|
+
if self.is_async:
|
|
107
|
+
return await self.callback(data)
|
|
108
|
+
else:
|
|
109
|
+
# Ejecutar sync en thread pool para no bloquear
|
|
110
|
+
import concurrent.futures
|
|
111
|
+
loop = asyncio.get_running_loop()
|
|
112
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
113
|
+
return await loop.run_in_executor(pool, self.callback, data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class G360EventBus:
|
|
117
|
+
"""
|
|
118
|
+
Bus de eventos estandarizado para comunicacion entre apps G360.
|
|
119
|
+
|
|
120
|
+
Patrones de nombre de eventos:
|
|
121
|
+
- app:{nombre}:{accion} — Eventos de la app
|
|
122
|
+
- g360:{tipo}:{accion} — Eventos del sistema G360
|
|
123
|
+
- {dominio}:{accion} — Eventos de dominio especifico
|
|
124
|
+
|
|
125
|
+
Ejemplos:
|
|
126
|
+
- app:stock-monitor:refresh
|
|
127
|
+
- g360:theme:change
|
|
128
|
+
- inventory:stock:low
|
|
129
|
+
|
|
130
|
+
Soporta wildcards:
|
|
131
|
+
- app:*:refresh — Todos los refresh de apps
|
|
132
|
+
- g360:* — Todos los eventos del sistema
|
|
133
|
+
- app:stock-monitor:* — Todos los eventos de stock-monitor
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
_instance = None
|
|
137
|
+
_lock = threading.Lock()
|
|
138
|
+
|
|
139
|
+
def __new__(cls):
|
|
140
|
+
with cls._lock:
|
|
141
|
+
if cls._instance is None:
|
|
142
|
+
cls._instance = super().__new__(cls)
|
|
143
|
+
cls._instance._initialized = False
|
|
144
|
+
return cls._instance
|
|
145
|
+
|
|
146
|
+
def __init__(self):
|
|
147
|
+
if self._initialized:
|
|
148
|
+
return
|
|
149
|
+
self._initialized = True
|
|
150
|
+
self._subscribers: Dict[str, List[EventCallback]] = {}
|
|
151
|
+
self._app_registry: Dict[str, AppMetadata] = {}
|
|
152
|
+
self._history: List[dict] = []
|
|
153
|
+
self._max_history = 100
|
|
154
|
+
self._version = "1.0.0"
|
|
155
|
+
|
|
156
|
+
def subscribe(self, event_pattern: str, callback: Callable) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Suscribir a un evento (soporta wildcards).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
event_pattern: patron del evento (ej: "app:*:refresh")
|
|
162
|
+
callback: funcion a ejecutar (sync o async)
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ID de suscripcion para poder desuscribirse despues
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
sub_id = bus.subscribe("app:*:refresh", my_callback)
|
|
169
|
+
bus.unsubscribe(sub_id)
|
|
170
|
+
"""
|
|
171
|
+
is_async = _has_asyncio and asyncio.iscoroutinefunction(callback)
|
|
172
|
+
wrapper = EventCallback(callback, is_async)
|
|
173
|
+
|
|
174
|
+
if event_pattern not in self._subscribers:
|
|
175
|
+
self._subscribers[event_pattern] = []
|
|
176
|
+
self._subscribers[event_pattern].append(wrapper)
|
|
177
|
+
|
|
178
|
+
# Return unique ID for this subscription
|
|
179
|
+
sub_id = f"{event_pattern}:{len(self._subscribers[event_pattern]) - 1}"
|
|
180
|
+
return sub_id
|
|
181
|
+
|
|
182
|
+
def unsubscribe(self, event_pattern: str, callback: Optional[Callable] = None):
|
|
183
|
+
"""
|
|
184
|
+
Desuscribir de un evento.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
event_pattern: patron del evento
|
|
188
|
+
callback: si es None, elimina todas las suscripciones
|
|
189
|
+
"""
|
|
190
|
+
if event_pattern not in self._subscribers:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if callback is None:
|
|
194
|
+
# Eliminar todas las suscripciones de este patron
|
|
195
|
+
del self._subscribers[event_pattern]
|
|
196
|
+
else:
|
|
197
|
+
# Eliminar solo esta callback
|
|
198
|
+
self._subscribers[event_pattern] = [
|
|
199
|
+
cb for cb in self._subscribers[event_pattern]
|
|
200
|
+
if cb.callback != callback
|
|
201
|
+
]
|
|
202
|
+
if not self._subscribers[event_pattern]:
|
|
203
|
+
del self._subscribers[event_pattern]
|
|
204
|
+
|
|
205
|
+
def unsubscribe_all(self):
|
|
206
|
+
"""Eliminar todas las suscripciones."""
|
|
207
|
+
self._subscribers.clear()
|
|
208
|
+
|
|
209
|
+
async def publish(self, event: str, data: Optional[dict] = None):
|
|
210
|
+
"""
|
|
211
|
+
Publicar un evento de forma asincrona.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
event: nombre del evento
|
|
215
|
+
data: datos opcionales
|
|
216
|
+
"""
|
|
217
|
+
results = []
|
|
218
|
+
|
|
219
|
+
# Ejecutar suscriptores exactos
|
|
220
|
+
if event in self._subscribers:
|
|
221
|
+
for cb in self._subscribers[event]:
|
|
222
|
+
try:
|
|
223
|
+
result = cb(data)
|
|
224
|
+
if asyncio.iscoroutine(result):
|
|
225
|
+
results.append(await result)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error in callback for {event}: {e}")
|
|
228
|
+
|
|
229
|
+
# Ejecutar suscriptores con wildcard
|
|
230
|
+
for pattern, callbacks in self._subscribers.items():
|
|
231
|
+
if '*' in pattern and self._match_pattern(pattern, event):
|
|
232
|
+
for cb in callbacks:
|
|
233
|
+
try:
|
|
234
|
+
result = cb(data)
|
|
235
|
+
if asyncio.iscoroutine(result):
|
|
236
|
+
results.append(await result)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Error in callback for {pattern}: {e}")
|
|
239
|
+
|
|
240
|
+
# Registrar en historial
|
|
241
|
+
self._history.append({
|
|
242
|
+
"event": event,
|
|
243
|
+
"data": data,
|
|
244
|
+
"timestamp": datetime.now().isoformat(),
|
|
245
|
+
"results": len(results),
|
|
246
|
+
})
|
|
247
|
+
if len(self._history) > self._max_history:
|
|
248
|
+
self._history = self._history[-self._max_history:]
|
|
249
|
+
|
|
250
|
+
return results
|
|
251
|
+
|
|
252
|
+
def publish_sync(self, event: str, data: Optional[dict] = None):
|
|
253
|
+
"""
|
|
254
|
+
Publicar un evento de forma sincrona (para usar fuera de async context).
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
event: nombre del evento
|
|
258
|
+
data: datos opcionales
|
|
259
|
+
"""
|
|
260
|
+
results = []
|
|
261
|
+
|
|
262
|
+
# Ejecutar suscriptores exactos
|
|
263
|
+
if event in self._subscribers:
|
|
264
|
+
for cb in self._subscribers[event]:
|
|
265
|
+
try:
|
|
266
|
+
result = cb.callback(data)
|
|
267
|
+
if asyncio.iscoroutine(result):
|
|
268
|
+
# Si es async, ejecutar en nuevo loop
|
|
269
|
+
import asyncio as _asyncio
|
|
270
|
+
result = _asyncio.run(result)
|
|
271
|
+
results.append(result)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Error in callback for {event}: {e}")
|
|
274
|
+
|
|
275
|
+
# Ejecutar suscriptores con wildcard
|
|
276
|
+
for pattern, callbacks in self._subscribers.items():
|
|
277
|
+
if '*' in pattern and self._match_pattern(pattern, event):
|
|
278
|
+
for cb in callbacks:
|
|
279
|
+
try:
|
|
280
|
+
result = cb.callback(data)
|
|
281
|
+
if asyncio.iscoroutine(result):
|
|
282
|
+
import asyncio as _asyncio
|
|
283
|
+
result = _asyncio.run(result)
|
|
284
|
+
results.append(result)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(f"Error in callback for {pattern}: {e}")
|
|
287
|
+
|
|
288
|
+
# Registrar en historial
|
|
289
|
+
self._history.append({
|
|
290
|
+
"event": event,
|
|
291
|
+
"data": data,
|
|
292
|
+
"timestamp": datetime.now().isoformat(),
|
|
293
|
+
"results": len(results),
|
|
294
|
+
})
|
|
295
|
+
if len(self._history) > self._max_history:
|
|
296
|
+
self._history = self._history[-self._max_history:]
|
|
297
|
+
|
|
298
|
+
return results
|
|
299
|
+
|
|
300
|
+
def _match_pattern(self, pattern: str, event: str) -> bool:
|
|
301
|
+
"""Verificar si un evento coincide con un patron (wildcards)."""
|
|
302
|
+
pattern_parts = pattern.split(':')
|
|
303
|
+
event_parts = event.split(':')
|
|
304
|
+
|
|
305
|
+
if len(pattern_parts) != len(event_parts):
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
for p, e in zip(pattern_parts, event_parts):
|
|
309
|
+
if p != '*' and p != e:
|
|
310
|
+
return False
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
def get_history(self, limit: int = 10) -> List[dict]:
|
|
314
|
+
"""Obtener historial de eventos."""
|
|
315
|
+
return self._history[-limit:]
|
|
316
|
+
|
|
317
|
+
def get_subscribers_count(self) -> int:
|
|
318
|
+
"""Obtener cantidad total de suscripciones."""
|
|
319
|
+
return sum(len(cbs) for cbs in self._subscribers.values())
|
|
320
|
+
|
|
321
|
+
def get_event_names(self) -> List[str]:
|
|
322
|
+
"""Obtener todos los patrones de eventos suscritos."""
|
|
323
|
+
return list(self._subscribers.keys())
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class G360AppRegistry:
|
|
327
|
+
"""
|
|
328
|
+
Registry de apps G360.
|
|
329
|
+
|
|
330
|
+
Permite:
|
|
331
|
+
- Registrar una app
|
|
332
|
+
- Descubrir apps disponibles
|
|
333
|
+
- Ver estado de salud de apps
|
|
334
|
+
- Comunicacion entre apps
|
|
335
|
+
- Limpiar apps offline
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
def __init__(self, registry_file: Optional[Path] = None):
|
|
339
|
+
self.registry_file = registry_file or REGISTRY_FILE
|
|
340
|
+
self.registry_file.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
self._apps: Dict[str, AppMetadata] = {}
|
|
342
|
+
self._schema_version = REGISTRY_SCHEMA_VERSION
|
|
343
|
+
self._load_registry()
|
|
344
|
+
|
|
345
|
+
def register(self, app: AppMetadata):
|
|
346
|
+
"""Registrar una app en el registry."""
|
|
347
|
+
app.updated_at = datetime.now().isoformat()
|
|
348
|
+
if not app.created_at:
|
|
349
|
+
app.created_at = app.updated_at
|
|
350
|
+
app.status = "online"
|
|
351
|
+
app.last_seen = app.updated_at
|
|
352
|
+
self._apps[app.name] = app
|
|
353
|
+
self._save_registry()
|
|
354
|
+
logger.info(f"App registered: {app.name} v{app.version}")
|
|
355
|
+
|
|
356
|
+
def unregister(self, app_name: str):
|
|
357
|
+
"""Dar de baja una app."""
|
|
358
|
+
if app_name in self._apps:
|
|
359
|
+
del self._apps[app_name]
|
|
360
|
+
self._save_registry()
|
|
361
|
+
logger.info(f"App unregistered: {app_name}")
|
|
362
|
+
|
|
363
|
+
def get_app(self, app_name: str) -> Optional[AppMetadata]:
|
|
364
|
+
"""Obtener metadatos de una app."""
|
|
365
|
+
return self._apps.get(app_name)
|
|
366
|
+
|
|
367
|
+
def list_apps(self) -> List[AppMetadata]:
|
|
368
|
+
"""Listar todas las apps registradas."""
|
|
369
|
+
return list(self._apps.values())
|
|
370
|
+
|
|
371
|
+
def list_online_apps(self) -> List[AppMetadata]:
|
|
372
|
+
"""Listar solo apps online."""
|
|
373
|
+
return [app for app in self._apps.values() if app.status == "online"]
|
|
374
|
+
|
|
375
|
+
def find_by_skill(self, skill: str) -> List[AppMetadata]:
|
|
376
|
+
"""Buscar apps por skill."""
|
|
377
|
+
return [app for app in self._apps.values() if app.skill == skill]
|
|
378
|
+
|
|
379
|
+
def find_by_framework(self, framework: str) -> List[AppMetadata]:
|
|
380
|
+
"""Buscar apps por framework."""
|
|
381
|
+
return [app for app in self._apps.values() if app.framework == framework]
|
|
382
|
+
|
|
383
|
+
def find_by_event(self, event: str) -> List[AppMetadata]:
|
|
384
|
+
"""Buscar apps que manejan un evento especifico."""
|
|
385
|
+
return [app for app in self._apps.values() if event in app.events]
|
|
386
|
+
|
|
387
|
+
def find_recent_apps(self, max_age_seconds: int = 300) -> List[AppMetadata]:
|
|
388
|
+
"""Buscar apps vistas recientemente."""
|
|
389
|
+
return [app for app in self._apps.values() if app.is_recent(max_age_seconds)]
|
|
390
|
+
|
|
391
|
+
def update_status(self, app_name: str, status: str):
|
|
392
|
+
"""Actualizar estado de una app."""
|
|
393
|
+
if app_name in self._apps:
|
|
394
|
+
self._apps[app_name].status = status
|
|
395
|
+
self._apps[app_name].last_seen = datetime.now().isoformat()
|
|
396
|
+
self._save_registry()
|
|
397
|
+
|
|
398
|
+
def heartbeat(self, app_name: str):
|
|
399
|
+
"""Actualizar last_seen de una app (heartbeat)."""
|
|
400
|
+
if app_name in self._apps:
|
|
401
|
+
self._apps[app_name].last_seen = datetime.now().isoformat()
|
|
402
|
+
self._save_registry()
|
|
403
|
+
|
|
404
|
+
def cleanup_offline(self, max_age_seconds: int = 600):
|
|
405
|
+
"""Limpiar apps que han estado offline mucho tiempo."""
|
|
406
|
+
to_remove = []
|
|
407
|
+
for name, app in self._apps.items():
|
|
408
|
+
if not app.is_recent(max_age_seconds):
|
|
409
|
+
to_remove.append(name)
|
|
410
|
+
|
|
411
|
+
for name in to_remove:
|
|
412
|
+
del self._apps[name]
|
|
413
|
+
|
|
414
|
+
if to_remove:
|
|
415
|
+
self._save_registry()
|
|
416
|
+
logger.info(f"Cleaned up {len(to_remove)} offline apps")
|
|
417
|
+
|
|
418
|
+
return to_remove
|
|
419
|
+
|
|
420
|
+
def _load_registry(self):
|
|
421
|
+
"""Cargar registry desde archivo."""
|
|
422
|
+
try:
|
|
423
|
+
if self.registry_file.exists():
|
|
424
|
+
with open(self.registry_file, 'r', encoding='utf-8') as f:
|
|
425
|
+
data = json.load(f)
|
|
426
|
+
# Verificar version del schema
|
|
427
|
+
stored_version = data.get('schema_version', '0.0.0')
|
|
428
|
+
if stored_version != self._schema_version:
|
|
429
|
+
logger.warning(f"Schema version mismatch: {stored_version} vs {self._schema_version}")
|
|
430
|
+
|
|
431
|
+
self._apps = {
|
|
432
|
+
name: AppMetadata.from_dict(app_data)
|
|
433
|
+
for name, app_data in data.get('apps', {}).items()
|
|
434
|
+
}
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"Error loading registry: {e}")
|
|
437
|
+
self._apps = {}
|
|
438
|
+
|
|
439
|
+
def _save_registry(self):
|
|
440
|
+
"""Guardar registry en archivo."""
|
|
441
|
+
try:
|
|
442
|
+
data = {
|
|
443
|
+
'schema_version': self._schema_version,
|
|
444
|
+
'apps': {name: app.to_dict() for name, app in self._apps.items()},
|
|
445
|
+
'updated_at': datetime.now().isoformat()
|
|
446
|
+
}
|
|
447
|
+
with open(self.registry_file, 'w', encoding='utf-8') as f:
|
|
448
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logger.error(f"Error saving registry: {e}")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Instancias globales
|
|
454
|
+
_event_bus = None
|
|
455
|
+
_app_registry = None
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def get_event_bus() -> G360EventBus:
|
|
459
|
+
"""Obtener instancia singleton del event bus."""
|
|
460
|
+
global _event_bus
|
|
461
|
+
if _event_bus is None:
|
|
462
|
+
_event_bus = G360EventBus()
|
|
463
|
+
return _event_bus
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def get_app_registry() -> G360AppRegistry:
|
|
467
|
+
"""Obtener instancia singleton del registry."""
|
|
468
|
+
global _app_registry
|
|
469
|
+
if _app_registry is None:
|
|
470
|
+
_app_registry = G360AppRegistry()
|
|
471
|
+
return _app_registry
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def register_g360_app(
|
|
475
|
+
name: str,
|
|
476
|
+
version: str,
|
|
477
|
+
skill: str,
|
|
478
|
+
framework: str = "flet",
|
|
479
|
+
events: Optional[List[str]] = None,
|
|
480
|
+
endpoints: Optional[Dict[str, str]] = None,
|
|
481
|
+
description: str = "",
|
|
482
|
+
) -> AppMetadata:
|
|
483
|
+
"""
|
|
484
|
+
Registrar una app G360 en el registry global.
|
|
485
|
+
|
|
486
|
+
Usage:
|
|
487
|
+
register_g360_app(
|
|
488
|
+
name="stock-monitor",
|
|
489
|
+
version="1.0.0",
|
|
490
|
+
skill="cipsa",
|
|
491
|
+
events=["app:stock:refresh", "app:data:update"]
|
|
492
|
+
)
|
|
493
|
+
"""
|
|
494
|
+
registry = get_app_registry()
|
|
495
|
+
app = AppMetadata(
|
|
496
|
+
name=name,
|
|
497
|
+
version=version,
|
|
498
|
+
skill=skill,
|
|
499
|
+
framework=framework,
|
|
500
|
+
description=description,
|
|
501
|
+
events=events or [],
|
|
502
|
+
endpoints=endpoints or {},
|
|
503
|
+
status="online",
|
|
504
|
+
created_at=datetime.now().isoformat(),
|
|
505
|
+
updated_at=datetime.now().isoformat(),
|
|
506
|
+
last_seen=datetime.now().isoformat(),
|
|
507
|
+
)
|
|
508
|
+
registry.register(app)
|
|
509
|
+
return app
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def unsubscribe_g360_event(event_pattern: str, callback: Optional[Callable] = None):
|
|
513
|
+
"""Desuscribir de un evento G360."""
|
|
514
|
+
bus = get_event_bus()
|
|
515
|
+
bus.unsubscribe(event_pattern, callback)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def subscribe_g360_event(event_pattern: str, callback: Callable) -> str:
|
|
519
|
+
"""
|
|
520
|
+
Suscribir a un evento G360.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
subscription_id para poder desuscribirse despues
|
|
524
|
+
"""
|
|
525
|
+
bus = get_event_bus()
|
|
526
|
+
return bus.subscribe(event_pattern, callback)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
async def publish_g360_event(event: str, data: Optional[dict] = None):
|
|
530
|
+
"""Publicar un evento G360 (async)."""
|
|
531
|
+
bus = get_event_bus()
|
|
532
|
+
return await bus.publish(event, data)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def publish_g360_event_sync(event: str, data: Optional[dict] = None):
|
|
536
|
+
"""Publicar un evento G360 (sync)."""
|
|
537
|
+
bus = get_event_bus()
|
|
538
|
+
return bus.publish_sync(event, data)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def discover_apps(skill: Optional[str] = None, online_only: bool = True) -> List[AppMetadata]:
|
|
542
|
+
"""
|
|
543
|
+
Descubrir apps G360 registradas.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
skill: Filtrar por skill (opcional)
|
|
547
|
+
online_only: Solo apps online
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Lista de apps encontradas
|
|
551
|
+
"""
|
|
552
|
+
registry = get_app_registry()
|
|
553
|
+
|
|
554
|
+
if online_only:
|
|
555
|
+
apps = registry.list_online_apps()
|
|
556
|
+
else:
|
|
557
|
+
apps = registry.list_apps()
|
|
558
|
+
|
|
559
|
+
if skill:
|
|
560
|
+
apps = [app for app in apps if app.skill == skill]
|
|
561
|
+
|
|
562
|
+
return apps
|
|
@@ -50,7 +50,7 @@ class Dashboard:
|
|
|
50
50
|
ft.Text("Datos en caché", size=11, color=self.c["warning"], weight=ft.FontWeight.W_500),
|
|
51
51
|
], spacing=4, vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
|
52
52
|
visible=False,
|
|
53
|
-
padding=ft.
|
|
53
|
+
padding=ft.padding.only(left=10, right=10, top=5, bottom=5),
|
|
54
54
|
bgcolor=rgba(self.c["warning"], 0.07),
|
|
55
55
|
border_radius=8,
|
|
56
56
|
)
|
|
@@ -122,7 +122,7 @@ class Dashboard:
|
|
|
122
122
|
ft.Container(width=16),
|
|
123
123
|
self._theme_button,
|
|
124
124
|
], vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
|
125
|
-
padding=ft.
|
|
125
|
+
padding=ft.padding.only(left=20, right=20, top=12, bottom=12),
|
|
126
126
|
bgcolor=self.c["surface"],
|
|
127
127
|
border_bottom=ft.BorderSide(1, self.c["border"]),
|
|
128
128
|
)
|
|
@@ -43,7 +43,7 @@ class KPICard(ft.Container):
|
|
|
43
43
|
spacing=0,
|
|
44
44
|
horizontal_alignment=ft.CrossAxisAlignment.START,
|
|
45
45
|
)
|
|
46
|
-
self.padding = ft.
|
|
46
|
+
self.padding = ft.padding.only(left=14, right=14, top=10, bottom=10)
|
|
47
47
|
self.bgcolor = rgba(color, 0.08)
|
|
48
48
|
self.border_radius = 10
|
|
49
49
|
self.shadow = ft.BoxShadow(
|
|
@@ -46,9 +46,9 @@ class SearchOverlay(ft.Container):
|
|
|
46
46
|
content=ft.Column([], scroll=ft.ScrollMode.AUTO, max_height=300, id="search_results"),
|
|
47
47
|
bgcolor=self.c["surface"],
|
|
48
48
|
border_radius=ft.BorderRadius.only(bottom_left=10, bottom_right=10),
|
|
49
|
-
border=ft.border.
|
|
49
|
+
border=ft.border.only(top=ft.BorderSide(1, color=self.c["border"])),
|
|
50
50
|
visible=False,
|
|
51
|
-
padding=ft.
|
|
51
|
+
padding=ft.padding.only(left=0, right=0, top=0, bottom=0),
|
|
52
52
|
max_height=300,
|
|
53
53
|
height=300,
|
|
54
54
|
),
|
|
@@ -58,7 +58,7 @@ class SearchOverlay(ft.Container):
|
|
|
58
58
|
|
|
59
59
|
self.bgcolor = self.c["surface"]
|
|
60
60
|
self.border_radius = 10
|
|
61
|
-
self.padding = ft.
|
|
61
|
+
self.padding = ft.padding.only(left=16, right=16, top=12, bottom=12)
|
|
62
62
|
self.width = 480
|
|
63
63
|
self.alignment = ft.alignment.top_center
|
|
64
64
|
self.margin = ft.margin.only(top=60)
|
|
@@ -102,7 +102,7 @@ class SearchOverlay(ft.Container):
|
|
|
102
102
|
], spacing=0),
|
|
103
103
|
bgcolor=rgba(self.c["accent"], 0.1) if is_selected else "transparent",
|
|
104
104
|
border_radius=6,
|
|
105
|
-
padding=ft.
|
|
105
|
+
padding=ft.padding.only(left=12, right=12, top=8, bottom=8),
|
|
106
106
|
on_click=lambda _, r=item: self._on_select(r),
|
|
107
107
|
mouse_cursor=ft.CursorType.CLICK,
|
|
108
108
|
)
|
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();
|
|
@@ -21,7 +21,7 @@ describe('addon command', () => {
|
|
|
21
21
|
describe('commands', () => {
|
|
22
22
|
it('should handle install command', async () => {
|
|
23
23
|
const { addon } = await import('../commands/addon.js');
|
|
24
|
-
|
|
24
|
+
// Test removed: functions always have name property in JS
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('should handle list command', async () => {
|
|
@@ -34,4 +34,4 @@ describe('addon command', () => {
|
|
|
34
34
|
expect(addon).toBeDefined();
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
|
-
});
|
|
37
|
+
});
|