@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/INSTALACION.md +258 -0
- package/README.md +228 -0
- package/autoStart.js +608 -0
- package/autoupdate.js +473 -0
- package/autoupdate_renderer.js +371 -0
- package/bin/glassnote.js +523 -0
- package/images/demo01.png +0 -0
- package/images/dmg-background.png +0 -0
- package/images/dmg-background.svg +37628 -0
- package/images/icon-16.png +0 -0
- package/images/icon-24.png +0 -0
- package/images/icon-512.png +0 -0
- package/images/icon.ico +0 -0
- package/images/icon.png +0 -0
- package/images/splash.svg +61 -0
- package/localserver.js +297 -0
- package/logUtilities.js +128 -0
- package/main.js +745 -0
- package/npmMode.js +139 -0
- package/npmUpdate.js +152 -0
- package/package.json +141 -0
- package/preload.js +148 -0
- package/userData.js +492 -0
package/autoupdate.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
const { app } = require('electron');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { exec, spawn } = require('child_process');
|
|
7
|
+
const userData = require('./userData');
|
|
8
|
+
|
|
9
|
+
// El renderer se baja siempre del bucket de PRODUCCIÓN. Antes esto lo decidía un campo
|
|
10
|
+
// `stage` en package.json que elegía entre este bucket y `glassnotedev`: ese ambiente ya
|
|
11
|
+
// no existe, y el campo solo servía para publicar apuntando a un bucket muerto sin que se
|
|
12
|
+
// notara hasta que alguien viera un renderer viejo. Para probar contra otro origen está
|
|
13
|
+
// GLASSNOTE_S3_BASE_URL, que es explícito y no viaja en el paquete publicado.
|
|
14
|
+
const S3_BASE_URL = process.env.GLASSNOTE_S3_BASE_URL || 'https://glassnote.s3.us-east-1.amazonaws.com';
|
|
15
|
+
|
|
16
|
+
class AutoUpdate {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.S3_BASE_URL = S3_BASE_URL;
|
|
19
|
+
this.currentVersion = app.getVersion();
|
|
20
|
+
this.expectedHash = null;
|
|
21
|
+
this.updateInterval = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Inicia el sistema de auto-actualización
|
|
26
|
+
*/
|
|
27
|
+
start() {
|
|
28
|
+
// Primera verificación después de 5 segundos
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
this.checkForUpdates();
|
|
31
|
+
}, 5000);
|
|
32
|
+
|
|
33
|
+
// Verificación periódica cada hora (3600000 ms = 1 hora)
|
|
34
|
+
this.updateInterval = setInterval(() => {
|
|
35
|
+
this.checkForUpdates();
|
|
36
|
+
}, 60 * 60 * 3000); // 3 hora en milisegundos
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detiene el sistema de auto-actualización
|
|
41
|
+
*/
|
|
42
|
+
stop() {
|
|
43
|
+
if (this.updateInterval) {
|
|
44
|
+
clearInterval(this.updateInterval);
|
|
45
|
+
this.updateInterval = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verifica si hay actualizaciones disponibles
|
|
51
|
+
*/
|
|
52
|
+
async checkForUpdates() {
|
|
53
|
+
try {
|
|
54
|
+
const desiredVersion = userData.get('desiredVersion') || 'lastest';
|
|
55
|
+
|
|
56
|
+
if (!desiredVersion || desiredVersion === '') {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const versionUrl = this.getVersionUrl(desiredVersion);
|
|
61
|
+
const versionContent = await this.downloadTextFile(versionUrl);
|
|
62
|
+
if (!versionContent) {
|
|
63
|
+
console.error('Error downloading version file from:', versionUrl);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parsear contenido del archivo de versión
|
|
68
|
+
const versionParts = versionContent.trim().split(' ');
|
|
69
|
+
const newVersion = versionParts[0];
|
|
70
|
+
this.expectedHash = versionParts.length > 1 ? versionParts[1].trim() : null;
|
|
71
|
+
|
|
72
|
+
// Log informativo de versiones
|
|
73
|
+
|
|
74
|
+
const isLatestDesired = desiredVersion === 'lastest';
|
|
75
|
+
|
|
76
|
+
if (isLatestDesired) {
|
|
77
|
+
// Para "lastest" - instalar si current < remote
|
|
78
|
+
const versionComparison = this.compareVersions(this.currentVersion, newVersion);
|
|
79
|
+
if (versionComparison < 0) {
|
|
80
|
+
await this.downloadInstaller(newVersion);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Para versión específica - instalar si current != desired
|
|
84
|
+
if (this.currentVersion !== desiredVersion) {
|
|
85
|
+
await this.downloadInstaller(desiredVersion);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Error checking for updates:', error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Obtiene la URL del archivo de versión
|
|
96
|
+
*/
|
|
97
|
+
getVersionUrl(desiredVersion) {
|
|
98
|
+
const platform = process.platform;
|
|
99
|
+
let platformName = 'windows';
|
|
100
|
+
|
|
101
|
+
if (platform === 'darwin') platformName = 'macos';
|
|
102
|
+
if (platform === 'linux') platformName = 'linux';
|
|
103
|
+
|
|
104
|
+
const versionPrefix = desiredVersion === 'lastest' ? '' : `${desiredVersion}_`;
|
|
105
|
+
return `${this.S3_BASE_URL}/version_${versionPrefix}${platformName}.txt`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Descarga un archivo desde una URL (para archivos de texto)
|
|
110
|
+
*/
|
|
111
|
+
downloadTextFile(url) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
https.get(url, (response) => {
|
|
114
|
+
if (response.statusCode !== 200) {
|
|
115
|
+
console.error(`HTTP error ${response.statusCode} downloading text file from ${url}`);
|
|
116
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let data = '';
|
|
121
|
+
response.on('data', (chunk) => {
|
|
122
|
+
data += chunk;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
response.on('end', () => {
|
|
126
|
+
resolve(data);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
}).on('error', (error) => {
|
|
130
|
+
console.error(`Error downloading text file from ${url}:`, error.message);
|
|
131
|
+
reject(error);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Descarga un archivo binario desde una URL
|
|
138
|
+
*/
|
|
139
|
+
downloadBinaryFile(url) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
https.get(url, (response) => {
|
|
142
|
+
if (response.statusCode !== 200) {
|
|
143
|
+
console.error(`HTTP error ${response.statusCode} downloading binary file from ${url}`);
|
|
144
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let totalBytes = 0;
|
|
150
|
+
|
|
151
|
+
response.on('data', (chunk) => {
|
|
152
|
+
chunks.push(chunk);
|
|
153
|
+
totalBytes += chunk.length;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
response.on('end', () => {
|
|
157
|
+
const buffer = Buffer.concat(chunks);
|
|
158
|
+
resolve(buffer);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
}).on('error', (error) => {
|
|
162
|
+
console.error(`Error downloading binary file from ${url}:`, error.message);
|
|
163
|
+
reject(error);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compara dos versiones semánticas
|
|
170
|
+
*/
|
|
171
|
+
compareVersions(version1, version2) {
|
|
172
|
+
const v1Parts = version1.split('.').map(part => parseInt(part) || 0);
|
|
173
|
+
const v2Parts = version2.split('.').map(part => parseInt(part) || 0);
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
176
|
+
const v1 = i < v1Parts.length ? v1Parts[i] : 0;
|
|
177
|
+
const v2 = i < v2Parts.length ? v2Parts[i] : 0;
|
|
178
|
+
|
|
179
|
+
if (v1 > v2) return 1;
|
|
180
|
+
if (v1 < v2) return -1;
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Obtiene la extensión del instalador según la plataforma
|
|
187
|
+
*/
|
|
188
|
+
getInstallerExtension() {
|
|
189
|
+
switch (process.platform) {
|
|
190
|
+
case 'darwin': return '.dmg';
|
|
191
|
+
case 'linux': return '.AppImage';
|
|
192
|
+
default: return '.exe';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Obtiene el nombre del archivo del instalador según la plataforma
|
|
198
|
+
*/
|
|
199
|
+
getInstallerFilename() {
|
|
200
|
+
return process.platform === 'linux' ? 'glassnote' : 'glassnote-installer';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Obtiene la ruta local del instalador
|
|
205
|
+
*/
|
|
206
|
+
getInstallerPath(version) {
|
|
207
|
+
const filename = `${this.getInstallerFilename()}-${version}${this.getInstallerExtension()}`;
|
|
208
|
+
return path.join(userData.getUserDataDir(), filename);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Descarga el instalador con reintentos y verificación robusta
|
|
213
|
+
*/
|
|
214
|
+
async downloadInstaller(version, maxRetries = 3) {
|
|
215
|
+
let retryCount = 0;
|
|
216
|
+
|
|
217
|
+
while (retryCount <= maxRetries) {
|
|
218
|
+
try {
|
|
219
|
+
const installerPath = this.getInstallerPath(version);
|
|
220
|
+
const installerUrl = `${this.S3_BASE_URL}/${this.getInstallerFilename()}-${version}${this.getInstallerExtension()}`;
|
|
221
|
+
|
|
222
|
+
// Verificar si el instalador ya existe y tiene el hash correcto
|
|
223
|
+
if (fs.existsSync(installerPath)) {
|
|
224
|
+
if (this.expectedHash) {
|
|
225
|
+
const fileHash = await this.calculateFileHash(installerPath);
|
|
226
|
+
|
|
227
|
+
// Comparación case-insensitive del hash
|
|
228
|
+
if (fileHash.toLowerCase() === this.expectedHash.toLowerCase()) {
|
|
229
|
+
this.runInstaller(version);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Eliminar archivo corrupto antes de reintentar
|
|
233
|
+
fs.unlinkSync(installerPath);
|
|
234
|
+
} else {
|
|
235
|
+
fs.unlinkSync(installerPath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const installerBuffer = await this.downloadBinaryFile(installerUrl);
|
|
240
|
+
|
|
241
|
+
// Verificar que el archivo se descargó completamente
|
|
242
|
+
if (!installerBuffer || installerBuffer.length === 0) {
|
|
243
|
+
throw new Error('Downloaded installer is empty or incomplete');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Guardar el instalador
|
|
247
|
+
fs.writeFileSync(installerPath, installerBuffer);
|
|
248
|
+
|
|
249
|
+
// Verificar hash inmediatamente después de guardar
|
|
250
|
+
if (this.expectedHash) {
|
|
251
|
+
const downloadedHash = await this.calculateBufferHash(installerBuffer);
|
|
252
|
+
|
|
253
|
+
// Comparación case-insensitive del hash
|
|
254
|
+
if (downloadedHash.toLowerCase() !== this.expectedHash.toLowerCase()) {
|
|
255
|
+
// Eliminar el archivo corrupto
|
|
256
|
+
fs.unlinkSync(installerPath);
|
|
257
|
+
throw new Error(`Installer hash verification failed! Expected: ${this.expectedHash}, Got: ${downloadedHash}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.runInstaller(version);
|
|
262
|
+
return; // Éxito, salir del bucle
|
|
263
|
+
|
|
264
|
+
} catch (error) {
|
|
265
|
+
retryCount++;
|
|
266
|
+
console.error(`Error downloading installer (attempt ${retryCount}/${maxRetries + 1}):`, error.message);
|
|
267
|
+
|
|
268
|
+
if (retryCount > maxRetries) {
|
|
269
|
+
console.error('All download attempts failed. Giving up.');
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Esperar antes de reintentar (backoff exponencial)
|
|
274
|
+
const waitTime = Math.pow(2, retryCount) * 1000;
|
|
275
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Calcula el hash SHA256 de un archivo
|
|
282
|
+
*/
|
|
283
|
+
calculateFileHash(filePath) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const hash = crypto.createHash('sha256');
|
|
286
|
+
const stream = fs.createReadStream(filePath);
|
|
287
|
+
|
|
288
|
+
stream.on('data', (data) => {
|
|
289
|
+
hash.update(data);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
stream.on('end', () => {
|
|
293
|
+
resolve(hash.digest('hex'));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
stream.on('error', (error) => {
|
|
297
|
+
reject(error);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Calcula el hash SHA256 de un buffer
|
|
304
|
+
*/
|
|
305
|
+
calculateBufferHash(buffer) {
|
|
306
|
+
const hash = crypto.createHash('sha256');
|
|
307
|
+
hash.update(buffer);
|
|
308
|
+
return hash.digest('hex');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Ejecuta el instalador
|
|
313
|
+
*/
|
|
314
|
+
runInstaller(version) {
|
|
315
|
+
const installerPath = this.getInstallerPath(version);
|
|
316
|
+
|
|
317
|
+
// Verificar hash si se proporcionó expectedHash
|
|
318
|
+
if (this.expectedHash) {
|
|
319
|
+
this.calculateFileHash(installerPath)
|
|
320
|
+
.then((fileHash) => {
|
|
321
|
+
// Comparación case-insensitive del hash
|
|
322
|
+
if (fileHash.toLowerCase() !== this.expectedHash.toLowerCase()) {
|
|
323
|
+
console.error('Installer hash verification failed!');
|
|
324
|
+
console.error('Expected:', this.expectedHash);
|
|
325
|
+
console.error('Actual:', fileHash);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this.executeInstaller(installerPath);
|
|
329
|
+
})
|
|
330
|
+
.catch((error) => {
|
|
331
|
+
console.error('Error verifying installer hash:', error);
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
this.executeInstaller(installerPath);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Ejecuta el instalador según la plataforma
|
|
340
|
+
*/
|
|
341
|
+
executeInstaller(installerPath) {
|
|
342
|
+
try {
|
|
343
|
+
if (process.platform === 'darwin') {
|
|
344
|
+
// macOS - manejo especial para DMG
|
|
345
|
+
this.executeMacInstaller(installerPath);
|
|
346
|
+
} else {
|
|
347
|
+
// Windows y Linux - ejecución normal
|
|
348
|
+
const isLinux = process.platform === 'linux';
|
|
349
|
+
|
|
350
|
+
// Ejecutar el instalador de forma asíncrona en un proceso independiente
|
|
351
|
+
let command;
|
|
352
|
+
if (process.platform === 'win32') {
|
|
353
|
+
// Windows: usar start para ejecutar en proceso independiente
|
|
354
|
+
command = `start "" "${installerPath}"`;
|
|
355
|
+
} else if (isLinux) {
|
|
356
|
+
// Linux: ejecutar instalador en background
|
|
357
|
+
command = `chmod +x "${installerPath}" && "${installerPath}" &`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
// Ejecutar el instalador en un proceso independiente
|
|
362
|
+
exec(command, (error) => {
|
|
363
|
+
if (error) {
|
|
364
|
+
console.error('Error executing installer:', error);
|
|
365
|
+
}
|
|
366
|
+
// El instalador se está ejecutando independientemente,
|
|
367
|
+
// ahora podemos cerrar la aplicación limpiamente
|
|
368
|
+
app.quit();
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error('Error executing installer:', error);
|
|
373
|
+
// En caso de error, asegurar cierre de la aplicación
|
|
374
|
+
app.quit();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Maneja la instalación en macOS (DMG)
|
|
380
|
+
*/
|
|
381
|
+
executeMacInstaller(installerPath) {
|
|
382
|
+
// Crear script de instalación automática para macOS
|
|
383
|
+
const installScript = this.createMacInstallScript(installerPath);
|
|
384
|
+
const scriptPath = path.join(app.getPath('temp'), 'glassnote_install.sh');
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Guardar el script
|
|
388
|
+
fs.writeFileSync(scriptPath, installScript, { mode: 0o755 });
|
|
389
|
+
|
|
390
|
+
// Ejecutar el script (que cerrará la app y hará la instalación)
|
|
391
|
+
exec(`"${scriptPath}"`, (error) => {
|
|
392
|
+
if (error) {
|
|
393
|
+
console.error('Error executing install script:', error);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// El script se encarga de cerrar la aplicación
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Error creating install script:', error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Crea un script de instalación para macOS
|
|
405
|
+
*/
|
|
406
|
+
createMacInstallScript(installerPath) {
|
|
407
|
+
const appName = 'glassnote.app';
|
|
408
|
+
const tempMountPoint = '/Volumes/GlassNoteInstaller';
|
|
409
|
+
const applicationsDir = '/Applications';
|
|
410
|
+
|
|
411
|
+
return `#!/bin/bash
|
|
412
|
+
|
|
413
|
+
# Script de instalación automática para GlassNote macOS
|
|
414
|
+
|
|
415
|
+
echo " Iniciando instalación automática de GlassNote..."
|
|
416
|
+
|
|
417
|
+
# Cerrar la aplicación si está ejecutándose
|
|
418
|
+
echo " Cerrando GlassNote si está en ejecución..."
|
|
419
|
+
pkill -f "GlassNote" || true
|
|
420
|
+
sleep 2
|
|
421
|
+
|
|
422
|
+
# Asegurarse de que la aplicación esté completamente cerrada
|
|
423
|
+
pkill -9 -f "GlassNote" || true
|
|
424
|
+
sleep 1
|
|
425
|
+
|
|
426
|
+
# Desmontar punto de montaje si existe
|
|
427
|
+
echo " Desmontando imagen previa si existe..."
|
|
428
|
+
hdiutil detach "${tempMountPoint}" -force 2>/dev/null || true
|
|
429
|
+
|
|
430
|
+
# Montar el DMG
|
|
431
|
+
echo " Montando imagen de instalación..."
|
|
432
|
+
hdiutil attach "${installerPath}" -nobrowse -mountpoint "${tempMountPoint}"
|
|
433
|
+
|
|
434
|
+
# Verificar que se montó correctamente
|
|
435
|
+
if [ ! -d "${tempMountPoint}/${appName}" ]; then
|
|
436
|
+
echo " Error: No se pudo encontrar ${appName} en la imagen DMG"
|
|
437
|
+
hdiutil detach "${tempMountPoint}" -force
|
|
438
|
+
exit 1
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
# Eliminar versión anterior si existe
|
|
442
|
+
echo "️ Eliminando versión anterior si existe..."
|
|
443
|
+
rm -rf "${applicationsDir}/${appName}"
|
|
444
|
+
|
|
445
|
+
# Copiar la nueva aplicación
|
|
446
|
+
echo " Copiando nueva versión a Applications..."
|
|
447
|
+
cp -R "${tempMountPoint}/${appName}" "${applicationsDir}/"
|
|
448
|
+
|
|
449
|
+
# Ajustar permisos
|
|
450
|
+
echo " Ajustando permisos..."
|
|
451
|
+
chmod -R 755 "${applicationsDir}/${appName}"
|
|
452
|
+
|
|
453
|
+
# Desmontar la imagen
|
|
454
|
+
echo " Desmontando imagen..."
|
|
455
|
+
hdiutil detach "${tempMountPoint}" -force
|
|
456
|
+
|
|
457
|
+
# Limpiar archivo de instalación temporal
|
|
458
|
+
echo " Limpiando archivos temporales..."
|
|
459
|
+
rm -f "${installerPath}"
|
|
460
|
+
|
|
461
|
+
echo " Instalación completada exitosamente!"
|
|
462
|
+
echo " GlassNote ha sido actualizado a la última versión"
|
|
463
|
+
|
|
464
|
+
# Abrir la aplicación (opcional)
|
|
465
|
+
sleep 2
|
|
466
|
+
open -a "${applicationsDir}/${appName}"
|
|
467
|
+
|
|
468
|
+
exit 0
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = AutoUpdate;
|