@glassnote/client 2.4.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/main.js ADDED
@@ -0,0 +1,745 @@
1
+ const {
2
+ app,
3
+ BrowserWindow,
4
+ Notification,
5
+ Tray,
6
+ Menu,
7
+ screen,
8
+ } = require('electron');
9
+ const packageJson = require('./package.json');
10
+ const LocalServer = require('./localserver');
11
+ const userData = require('./userData');
12
+ const { ipcMain } = require('electron');
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const AutoStart = require('./autoStart');
16
+ const AutoUpdate = require('./autoupdate');
17
+ const NpmUpdate = require('./npmUpdate');
18
+ const npmMode = require('./npmMode');
19
+ const AutoUpdateRenderer = require('./autoupdate_renderer');
20
+ const { createTailUtility } = require('./logUtilities');
21
+
22
+ const logRenderer = packageJson.logRenderer !== undefined ? packageJson.logRenderer : true;
23
+
24
+ // Sin empaquetar, electron llamaría a la app "Electron" en la bandeja, el menú de macOS
25
+ // y las notificaciones. Instalado por npm eso es siempre el caso, así que se nombra a
26
+ // mano. Los datos de usuario no dependen de esto (userData.js calcula su propia ruta).
27
+ app.setName('glassnote');
28
+
29
+ let mainWindow;
30
+ let localServer = null;
31
+ let tray = null;
32
+ let lockCheckerInterval = null;
33
+ let lockCheckerTimeout = null;
34
+ let isCleaningUp = false;
35
+
36
+
37
+ // Inicializar y pasar datos de usuario
38
+ const uuid = userData.getUUID();
39
+
40
+ // Configure logging - usar el directorio personalizado en lugar de Electron
41
+ const logsDir = path.join(userData.getUserDataDir(), 'logs');
42
+ if (!fs.existsSync(logsDir)) {
43
+ fs.mkdirSync(logsDir, { recursive: true });
44
+ }
45
+
46
+ // Clean up old logs (older than 30 days)
47
+ function cleanupOldLogs() {
48
+ try {
49
+ const files = fs.readdirSync(logsDir);
50
+ const now = Date.now();
51
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
52
+
53
+ files.forEach((file) => {
54
+ if (file.endsWith('.log')) {
55
+ const filePath = path.join(logsDir, file);
56
+ const stats = fs.statSync(filePath);
57
+ if (stats.mtimeMs < thirtyDaysAgo) {
58
+ fs.unlinkSync(filePath);
59
+ }
60
+ }
61
+ });
62
+ } catch (error) {
63
+ console.error('Error cleaning up old logs:', error);
64
+ }
65
+ }
66
+
67
+ // Create log file for current session
68
+ const currentDate = new Date().toISOString().split('T')[0];
69
+ const logFilePath = path.join(logsDir, `glassnote-${currentDate}.log`);
70
+
71
+ // Override console.log to write to file
72
+ const originalConsoleLog = console.log;
73
+ console.log = function (...args) {
74
+ const timestamp = new Date().toISOString();
75
+ const message = args
76
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
77
+ .join(' ');
78
+
79
+ const logMessage = `[${timestamp}] ${message}\n`;
80
+
81
+
82
+ // Write to file
83
+ try {
84
+ fs.appendFileSync(logFilePath, logMessage, 'utf8');
85
+ } catch (error) {
86
+ originalConsoleLog.apply(console, ['Error writing to log file:', error]);
87
+ }
88
+
89
+ // Call original console.log
90
+ originalConsoleLog.apply(console, args);
91
+ };
92
+ console.warn('====== APPLICATION START ======');
93
+ let initialLock = Date.now().toString();
94
+
95
+ // Override console.error as well
96
+ const originalConsoleError = console.error;
97
+ console.error = function (...args) {
98
+ const timestamp = new Date().toISOString();
99
+ const message = args
100
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
101
+ .join(' ');
102
+
103
+ const logMessage = `[${timestamp}] ERROR: ${message}\n`;
104
+
105
+ // Write to file
106
+ try {
107
+ fs.appendFileSync(logFilePath, logMessage, 'utf8');
108
+ } catch (error) {
109
+ originalConsoleError.apply(console, ['Error writing to log file:', error]);
110
+ }
111
+
112
+ // Call original console.error
113
+ originalConsoleError.apply(console, args);
114
+ };
115
+
116
+ // Clean up old logs on startup
117
+ cleanupOldLogs();
118
+
119
+ // Create tail utility in logs directory for debugging
120
+ createTailUtility(logsDir);
121
+
122
+ app.whenReady().then(() => {
123
+ // Configure auto-start - this will handle shortcut creation
124
+ //
125
+ // Corriendo por `npx` no se registra nada: la app vive en la caché de npx, que se
126
+ // limpia sola. Una entrada de arranque automático apuntando ahí queda rota sin que
127
+ // nadie se entere. Quien quiera que arranque solo, instala: `npx glassnote install`.
128
+ if (npmMode.isEphemeral()) {
129
+ console.log('[autostart] modo npx: no se registra el arranque automático');
130
+ } else {
131
+ try {
132
+ const autoStart = new AutoStart();
133
+ const autoStartResult = autoStart.configureAutoStart();
134
+ if (!autoStartResult) {
135
+ console.log('Auto-start configuration may have failed, but application will continue running normally');
136
+ }
137
+ } catch (error) {
138
+ console.error('Auto-start configuration failed, but application will continue running normally:', error.message);
139
+ }
140
+ }
141
+
142
+ // Configure auto-update
143
+ //
144
+ // Instalado por npm no hay instalador que bajar: actualizar es `npm install -g` y
145
+ // reabrir. El updater de S3 (.exe/.dmg) queda solo para los builds nativos, que
146
+ // están en deprecación — ver README, sección «Instalación».
147
+ const autoUpdate = npmMode.isNpmMode() ? new NpmUpdate() : new AutoUpdate();
148
+ autoUpdate.start();
149
+
150
+ // Create system tray with platform-appropriate icon
151
+ let iconPath;
152
+ if (process.platform === 'win32') {
153
+ iconPath = path.join(__dirname, 'images/icon.ico');
154
+ } else if (process.platform === 'darwin') {
155
+ // macOS uses smaller tray icons - use icon-16.png for proper sizing
156
+ iconPath = path.join(__dirname, 'images/icon-16.png');
157
+ } else {
158
+ iconPath = path.join(__dirname, 'images/icon.png');
159
+ }
160
+ tray = new Tray(iconPath);
161
+ const contextMenu = Menu.buildFromTemplate([
162
+ {
163
+ label: `glassnote v${packageJson.version}`,
164
+ enabled: false,
165
+ },
166
+ { type: 'separator' },
167
+ {
168
+ label: 'Config Menu',
169
+ click: () => {
170
+ // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
171
+ if (mainWindow && !mainWindow.isDestroyed()) {
172
+ mainWindow.webContents.send('show-config-menu', 'review');
173
+ }
174
+ }
175
+ }
176
+ ]);
177
+ tray.setToolTip(`GlassNote v${packageJson.version}`);
178
+ tray.setContextMenu(contextMenu);
179
+
180
+ // Agregar funcionalidad de click izquierdo para mostrar el Config Menu
181
+ tray.on('click', () => {
182
+ // Enviar mensaje al renderer para mostrar el ConfigMenu con vista Review
183
+ if (mainWindow && !mainWindow.isDestroyed()) {
184
+ mainWindow.webContents.send('show-config-menu', 'review');
185
+ }
186
+ });
187
+
188
+ // Platform-specific window configuration
189
+ // On macOS: Use screen dimensions instead of fullscreen to avoid virtual desktop behavior
190
+ // On Windows: Keep original fullscreen behavior
191
+ let windowOptions = {
192
+ transparent: true,
193
+ frame: false,
194
+ alwaysOnTop: true,
195
+ skipTaskbar: true,
196
+ resizable: false,
197
+ x: 0,
198
+ y: 0,
199
+ acceptFirstMouse: true,
200
+ webPreferences: {
201
+ nodeIntegration: false,
202
+ contextIsolation: true,
203
+ enableRemoteModule: false,
204
+ preload: path.join(__dirname, 'preload.js'),
205
+ sandbox: false, // Allow access to Node.js modules in preload
206
+ webSecurity: false, // Allow connections to local WebSocket servers
207
+ },
208
+ };
209
+
210
+ if (process.platform === 'darwin') {
211
+ // macOS
212
+ const primaryDisplay = screen.getPrimaryDisplay();
213
+ const { width, height } = primaryDisplay.workAreaSize;
214
+ windowOptions.width = width;
215
+ windowOptions.height = height;
216
+ // Explicitly set fullscreen to false for macOS
217
+ windowOptions.fullscreen = false;
218
+ } else {
219
+ // Windows and other platforms
220
+ windowOptions.width = 400;
221
+ windowOptions.height = 300;
222
+ windowOptions.fullscreen = true;
223
+ }
224
+
225
+ mainWindow = new BrowserWindow(windowOptions);
226
+
227
+ // Interceptar console.log de la página renderizada
228
+ if (logRenderer) {
229
+ mainWindow.webContents.on(
230
+ 'console-message',
231
+ (event, level, message, line, sourceId) => {
232
+ const levels = ['', 'INFO', 'WARNING', 'ERROR', 'DEBUG'];
233
+ const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
234
+ const logMessage = `[${timestamp}] [RENDERER ${levels[level]}] ${message} (${sourceId}:${line})`;
235
+
236
+ // Write to both console and log file
237
+ console.info(logMessage);
238
+ // Also write directly to log file to ensure it's captured
239
+ const fileLogMessage = `[${timestamp}] ${logMessage}\n`;
240
+ try {
241
+ fs.appendFileSync(logFilePath, fileLogMessage, 'utf8');
242
+ } catch (error) {
243
+ console.error('Error writing renderer log to file:', error);
244
+ }
245
+ }
246
+ );
247
+ }
248
+
249
+ // Generic userData operations via IPC
250
+ ipcMain.on('get-user-data', (event, { key, nestedKey }) => {
251
+ try {
252
+ const value = userData.get(key, nestedKey);
253
+
254
+ // Serialize the value for IPC communication
255
+ let serializableValue;
256
+ if (value === undefined || value === null) {
257
+ serializableValue = null;
258
+ } else {
259
+ // Convert to JSON string and back to ensure proper serialization
260
+ serializableValue = JSON.parse(JSON.stringify(value));
261
+ }
262
+
263
+ event.reply('user-data-response', { key, nestedKey, value: serializableValue, success: true });
264
+ } catch (error) {
265
+ console.error('Error getting user data for key:', key, 'nestedKey:', nestedKey, error);
266
+ event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
267
+ }
268
+ });
269
+
270
+ // Direct userData operation for IPC serialization issues
271
+ ipcMain.on('get-user-data-direct', (event, { key, nestedKey }) => {
272
+ try {
273
+ const value = userData.get(key, nestedKey);
274
+
275
+ // Use a different approach for direct communication
276
+ // Send the value as a JSON string to avoid IPC serialization issues
277
+ const response = {
278
+ key,
279
+ nestedKey,
280
+ value: value ? JSON.stringify(value) : null,
281
+ success: true
282
+ };
283
+
284
+ event.reply('user-data-direct-response', response);
285
+ } catch (error) {
286
+ console.error('Error getting user data directly for key:', key, 'nestedKey:', nestedKey, error);
287
+ event.reply('user-data-direct-response', { key, nestedKey, value: null, success: false, error: error.message });
288
+ }
289
+ });
290
+
291
+ ipcMain.on('set-user-data', (event, { key, nestedKey, value }) => {
292
+ try {
293
+ const success = userData.set(key, nestedKey, value);
294
+ event.reply('user-data-response', { key, nestedKey, value, success });
295
+
296
+ // If setting servers, send updated list to renderer
297
+ if (key === 'servers' && Array.isArray(value) && nestedKey === undefined) {
298
+ if (mainWindow && !mainWindow.isDestroyed()) {
299
+ mainWindow.webContents.send('servers-list', value);
300
+ }
301
+ }
302
+ } catch (error) {
303
+ console.error('Error setting user data for key:', key, 'nestedKey:', nestedKey, error);
304
+ event.reply('user-data-response', { key, nestedKey, value, success: false, error: error.message });
305
+ }
306
+ });
307
+
308
+ ipcMain.on('remove-user-data', (event, { key, nestedKey }) => {
309
+ try {
310
+ const success = userData.remove(key, nestedKey);
311
+ event.reply('user-data-response', { key, nestedKey, value: null, success });
312
+ } catch (error) {
313
+ console.error('Error removing user data for key:', key, 'nestedKey:', nestedKey, error);
314
+ event.reply('user-data-response', { key, nestedKey, value: null, success: false, error: error.message });
315
+ }
316
+ });
317
+
318
+ // Initialize renderer auto-update after mainWindow is created
319
+ const autoUpdateRenderer = new AutoUpdateRenderer((version) => {
320
+ // Recargar la ventana para aplicar los cambios
321
+ setTimeout(() => {
322
+ reloadMainWindow();
323
+ }, 1000);
324
+ });
325
+ autoUpdateRenderer.start();
326
+
327
+ // Cargar el HTML del renderer (priorizar userData, fallback a app)
328
+ loadRendererHtml(mainWindow, uuid);
329
+
330
+ // Mouse events control
331
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
332
+ ipcMain.on('set-ignore-events-true', () => {
333
+ mainWindow.setIgnoreMouseEvents(true, { forward: true });
334
+ });
335
+ ipcMain.on('set-ignore-events-false', () => {
336
+ mainWindow.setIgnoreMouseEvents(false, { forward: true });
337
+ });
338
+
339
+ // Window visibility control
340
+ ipcMain.on('show-window', () => {
341
+ if (mainWindow && !mainWindow.isDestroyed()) {
342
+ mainWindow.show();
343
+ }
344
+ });
345
+
346
+ ipcMain.on('hide-window', () => {
347
+ if (mainWindow && !mainWindow.isDestroyed()) {
348
+ mainWindow.hide();
349
+ }
350
+ });
351
+
352
+ mainWindow.setAlwaysOnTop(true, 'screen-saver');
353
+ mainWindow.webContents.on('did-finish-load', () => {
354
+ mainWindow.webContents.send('servers-list', userData.readData().servers);
355
+ mainWindow.webContents.send('app-data', { version: packageJson.version });
356
+ });
357
+
358
+
359
+ // Handle external URL opening
360
+ ipcMain.on('open-external', (event, url) => {
361
+ const { shell } = require('electron');
362
+ shell.openExternal(url);
363
+ });
364
+
365
+ ipcMain.on('open-external-browser', (event, url) => {
366
+ const { shell } = require('electron');
367
+ shell.openExternal(url);
368
+ });
369
+
370
+ // Handle registration window opening (larger window without menus)
371
+ ipcMain.on('open-registration-window', (event, url) => {
372
+ // Create a custom browser window for registration
373
+ const registrationWindow = new BrowserWindow({
374
+ width: 1200,
375
+ height: 800,
376
+ minWidth: 800,
377
+ minHeight: 600,
378
+ show: false,
379
+ titleBarStyle: 'default',
380
+ autoHideMenuBar: true,
381
+ webPreferences: {
382
+ nodeIntegration: false,
383
+ contextIsolation: true,
384
+ webSecurity: true
385
+ }
386
+ });
387
+
388
+ // Remove menu bar
389
+ registrationWindow.setMenuBarVisibility(false);
390
+
391
+ // Load the URL
392
+ registrationWindow.loadURL(url);
393
+
394
+ // Show window when ready
395
+ registrationWindow.once('ready-to-show', () => {
396
+ registrationWindow.show();
397
+ });
398
+
399
+ // Handle window closed
400
+ registrationWindow.on('closed', () => {
401
+ // Window closed, no action needed
402
+ });
403
+ });
404
+
405
+ mainWindow.on('ready-to-show', () => {
406
+ mainWindow.show();
407
+
408
+ // Use LocalServer singleton
409
+ if (!localServer) {
410
+ localServer = new LocalServer();
411
+ localServer.start(mainWindow);
412
+ } else {
413
+ // Update the mainWindow reference if server already exists
414
+ localServer.updateMainWindow(mainWindow);
415
+ }
416
+ });
417
+
418
+ lockChecker();
419
+ });
420
+
421
+ app.on('window-all-closed', () => {
422
+ // Don't quit when all windows are closed if we have a tray
423
+ if (tray && process.platform !== 'darwin') {
424
+ // Keep the app running with tray
425
+ } else {
426
+ cleanupAndQuit();
427
+ }
428
+ });
429
+
430
+ // Clean up on quit
431
+ app.on('before-quit', () => {
432
+ if (!isCleaningUp) {
433
+ // Clear intervals and timeouts
434
+ if (lockCheckerInterval) {
435
+ clearInterval(lockCheckerInterval);
436
+ lockCheckerInterval = null;
437
+ }
438
+ if (lockCheckerTimeout) {
439
+ clearTimeout(lockCheckerTimeout);
440
+ lockCheckerTimeout = null;
441
+ }
442
+
443
+ // Destroy main window
444
+ if (mainWindow && !mainWindow.isDestroyed()) {
445
+ mainWindow.destroy();
446
+ mainWindow = null;
447
+ }
448
+
449
+ // Stop local server
450
+ if (localServer) {
451
+ localServer.stop();
452
+ localServer = null;
453
+ }
454
+
455
+ // Destroy tray
456
+ if (tray) {
457
+ tray.destroy();
458
+ tray = null;
459
+ }
460
+ }
461
+ });
462
+
463
+ function lockChecker() {
464
+ lockCheckerTimeout = setTimeout(() => {
465
+ userData.set('lock', initialLock);
466
+ }, 1000);
467
+ lockCheckerInterval = setInterval(() => {
468
+ // Leer el lock directamente del archivo, no del cache
469
+ const currentData = userData.getLockFromFile();
470
+ // Si currentData es undefined, significa que el archivo .glassnote fue borrado o corrompido
471
+ // También verificar si el valor del lock cambió
472
+ if (currentData === undefined || currentData.toString() != initialLock.toString()) {
473
+ cleanupAndQuit();
474
+ }
475
+ }, 10000); // Check every 10 seconds
476
+ }
477
+
478
+ function cleanupAndQuit() {
479
+ // Prevent multiple cleanup calls
480
+ if (isCleaningUp) {
481
+ return;
482
+ }
483
+ isCleaningUp = true;
484
+
485
+
486
+ // Clear intervals and timeouts
487
+ if (lockCheckerInterval) {
488
+ clearInterval(lockCheckerInterval);
489
+ lockCheckerInterval = null;
490
+ }
491
+ if (lockCheckerTimeout) {
492
+ clearTimeout(lockCheckerTimeout);
493
+ lockCheckerTimeout = null;
494
+ }
495
+
496
+ // Destroy main window
497
+ if (mainWindow && !mainWindow.isDestroyed()) {
498
+ mainWindow.destroy();
499
+ mainWindow = null;
500
+ }
501
+
502
+ // Stop local server
503
+ if (localServer) {
504
+ localServer.stop();
505
+ localServer = null;
506
+ }
507
+
508
+ // Destroy tray
509
+ if (tray) {
510
+ tray.destroy();
511
+ tray = null;
512
+ }
513
+
514
+ // Force exit after a timeout to ensure process terminates
515
+ setTimeout(() => {
516
+ app.exit(0);
517
+ // Double safety: exit process after another timeout
518
+ setTimeout(() => {
519
+ process.exit(0);
520
+ }, 1000);
521
+ }, 500);
522
+ }
523
+
524
+ // Handle process signals for clean shutdown
525
+ process.on('SIGINT', () => {
526
+ cleanupAndQuit();
527
+ });
528
+
529
+ process.on('SIGTERM', () => {
530
+ cleanupAndQuit();
531
+ });
532
+
533
+ // Handle uncaught exceptions to prevent zombie processes
534
+ process.on('uncaughtException', (error) => {
535
+ console.error('Uncaught exception:', error);
536
+ cleanupAndQuit();
537
+ });
538
+
539
+ process.on('unhandledRejection', (reason, promise) => {
540
+ console.error('Unhandled promise rejection:', reason);
541
+ cleanupAndQuit();
542
+ });
543
+
544
+ // Start lock checker after everything is initialized
545
+
546
+
547
+
548
+ /**
549
+ * Carga el HTML del renderer desde la fuente con versión SUPERIOR.
550
+ *
551
+ * Dos fuentes, y la division importa:
552
+ * - userData: lo que bajo el auto-update de S3. Es el canal EN CALIENTE: una version
553
+ * nueva del renderer llega ahi sin republicar el cliente ni tocar el binario.
554
+ * - baseline: el renderer que viaja con la instalacion. Es lo que ve una instalacion
555
+ * nueva antes de su primer update en caliente.
556
+ *
557
+ * Gana la version mas alta, sin importar de donde venga; empate, gana userData.
558
+ */
559
+ // De donde sale el baseline. En una instalacion es la dependencia @glassnote/renderer,
560
+ // que npm resuelve sola. Pero si hay un clon del renderer al lado —el flujo de quien lo
561
+ // desarrolla, con `npm run watch-renderer`— ese gana: si no, editarlo no se veria nunca,
562
+ // tapado por la version publicada.
563
+ function baselineRendererDir() {
564
+ const clonLocal = path.join(__dirname, 'glassnote-renderer', 'dist');
565
+ if (fs.existsSync(clonLocal)) return clonLocal;
566
+ try {
567
+ return path.join(path.dirname(require.resolve('@glassnote/renderer/package.json')), 'dist');
568
+ } catch (error) {
569
+ // Sin dependencia ni clon solo queda userData; si tampoco esta, mas abajo se avisa.
570
+ return clonLocal;
571
+ }
572
+ }
573
+
574
+ function loadRendererHtml(window, uuid) {
575
+ // Definir las dos fuentes a considerar con archivos HTML alternativos
576
+ const sources = [
577
+ {
578
+ name: 'userData',
579
+ dir: path.join(userData.getUserDataDir(), 'renderer'),
580
+ htmlFiles: ['glassnote.html'], // userData siempre usa glassnote.html
581
+ type: 'auto-update'
582
+ },
583
+ {
584
+ name: 'baseline',
585
+ dir: baselineRendererDir(),
586
+ htmlFiles: ['glassnote.html', 'index.html'], // Intenta glassnote.html primero, luego index.html
587
+ type: 'paquete'
588
+ }
589
+ ];
590
+
591
+
592
+ // Obtener versiones y verificar archivos HTML para ambas fuentes
593
+ const sourcesWithInfo = sources.map(source => {
594
+ const version = getRendererVersionFromPath(source.dir);
595
+
596
+ // Buscar el primer archivo HTML que exista
597
+ let htmlPath = null;
598
+ let htmlFile = null;
599
+ for (const file of source.htmlFiles) {
600
+ const potentialPath = path.join(source.dir, file);
601
+ if (fs.existsSync(potentialPath)) {
602
+ htmlPath = potentialPath;
603
+ htmlFile = file;
604
+ break;
605
+ }
606
+ }
607
+
608
+ const htmlExists = htmlPath !== null;
609
+
610
+ return {
611
+ ...source,
612
+ version,
613
+ htmlPath,
614
+ htmlFile,
615
+ htmlExists
616
+ };
617
+ });
618
+
619
+ // Log de información encontrada
620
+ sourcesWithInfo.forEach(source => {
621
+ if (source.htmlExists) {
622
+ } else {
623
+ console.log(` ${source.name}: versión ${source.version}, SIN ARCHIVO HTML`);
624
+ }
625
+ });
626
+
627
+ // Comparar versiones para determinar cuál es superior
628
+ const versionComparison = compareVersions(
629
+ sourcesWithInfo[0].version,
630
+ sourcesWithInfo[1].version
631
+ );
632
+
633
+ // Determinar fuente con versión superior
634
+ let superiorSource;
635
+ if (versionComparison > 0) {
636
+ // userData tiene versión superior
637
+ superiorSource = sourcesWithInfo[0];
638
+ } else if (versionComparison < 0) {
639
+ // glassnote-renderer/dist tiene versión superior
640
+ superiorSource = sourcesWithInfo[1];
641
+ } else {
642
+ // Versiones iguales, priorizar userData (auto-update)
643
+ superiorSource = sourcesWithInfo[0];
644
+ }
645
+
646
+ // Intentar cargar desde la fuente con versión superior
647
+ if (superiorSource.htmlExists) {
648
+ window.loadFile(superiorSource.htmlPath, {
649
+ query: { uuid: uuid, version: packageJson.version }
650
+ });
651
+ return;
652
+ }
653
+
654
+ // Si la fuente con versión superior no tiene HTML, intentar la otra fuente
655
+ const otherSource = superiorSource.name === 'userData' ? sourcesWithInfo[1] : sourcesWithInfo[0];
656
+
657
+ if (otherSource.htmlExists) {
658
+
659
+ window.loadFile(otherSource.htmlPath, {
660
+ query: { uuid: uuid, version: packageJson.version }
661
+ });
662
+ return;
663
+ }
664
+
665
+ // Si ninguna fuente tiene HTML, error
666
+ console.error('ERROR: No se encontró archivo HTML del renderer en ninguna fuente');
667
+ console.error('userData buscó:', sourcesWithInfo[0].htmlFiles.join(', '));
668
+ console.error(`baseline (${sourcesWithInfo[1].dir}) buscó:`, sourcesWithInfo[1].htmlFiles.join(', '));
669
+
670
+ // Error fatal - no hay renderer disponible
671
+ console.error('ERROR FATAL: No se puede cargar el renderer');
672
+ // Intentar cargar una página de error o cerrar la aplicación
673
+ window.loadURL('data:text/html,<h1>Error: No se pudo cargar el renderer</h1>');
674
+ }
675
+
676
+ /**
677
+ * Compara dos versiones semánticas (formato: major.minor.patch)
678
+ * Retorna 1 si version1 > version2
679
+ * Retorna 0 si version1 == version2
680
+ * Retorna -1 si version1 < version2
681
+ */
682
+ function compareVersions(version1, version2) {
683
+ const v1Parts = version1.split('.').map((part) => parseInt(part) || 0);
684
+ const v2Parts = version2.split('.').map((part) => parseInt(part) || 0);
685
+
686
+ // Comparar partes principales (major, minor, patch)
687
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
688
+ const v1Part = v1Parts[i] || 0;
689
+ const v2Part = v2Parts[i] || 0;
690
+
691
+ if (v1Part > v2Part) return 1;
692
+ if (v1Part < v2Part) return -1;
693
+ }
694
+
695
+ return 0; // Las versiones son iguales
696
+ }
697
+
698
+ /**
699
+ * Obtiene la versión del renderer desde el package.json en el directorio especificado
700
+ * Si el directorio es un dist/ sin package.json, se mira el del paquete que lo contiene.
701
+ */
702
+ function getRendererVersionFromPath(rendererPath) {
703
+ try {
704
+ // Primero intentar en el directorio especificado
705
+ const packageJsonPath = path.join(rendererPath, 'package.json');
706
+ if (fs.existsSync(packageJsonPath)) {
707
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
708
+ return packageJson.version || '0.0.0';
709
+ }
710
+
711
+ // Un dist/ sin package.json propio: la version la tiene el paquete de arriba.
712
+ if (path.basename(rendererPath) === 'dist') {
713
+ const parentDir = path.join(rendererPath, '..'); // Subir un nivel
714
+ const parentPackageJsonPath = path.join(parentDir, 'package.json');
715
+ if (fs.existsSync(parentPackageJsonPath)) {
716
+ const packageJson = JSON.parse(fs.readFileSync(parentPackageJsonPath, 'utf8'));
717
+ return packageJson.version || '0.0.0';
718
+ }
719
+ }
720
+ } catch (error) {
721
+ console.error(
722
+ 'Error reading renderer version from',
723
+ rendererPath,
724
+ ':',
725
+ error
726
+ );
727
+ }
728
+ return '0.0.0'; // Versión por defecto si no existe o hay error
729
+ }
730
+
731
+ /**
732
+ * Recarga el main window después de una actualización del renderer
733
+ */
734
+ function reloadMainWindow() {
735
+ if (mainWindow && !mainWindow.isDestroyed()) {
736
+
737
+ // Update LocalServer with the new mainWindow reference
738
+ if (localServer) {
739
+ localServer.updateMainWindow(mainWindow);
740
+ }
741
+
742
+ loadRendererHtml(mainWindow, uuid);
743
+ }
744
+ }
745
+