@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/autoStart.js ADDED
@@ -0,0 +1,608 @@
1
+ // electron se carga de forma perezosa: este módulo también lo usa el CLI (node puro),
2
+ // que necesita `removeAutoStart()` para `glassnote uninstall`.
3
+ let app = null;
4
+ try {
5
+ ({ app } = require('electron'));
6
+ } catch (error) {
7
+ app = null;
8
+ }
9
+ const npmMode = require('./npmMode');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const { execSync, spawn } = require('child_process');
14
+
15
+ class AutoStart {
16
+ constructor() {
17
+ this.platform = process.platform;
18
+ }
19
+
20
+ // Comando con el que se vuelve a arrancar el cliente.
21
+ //
22
+ // Instalado por npm/npx no hay ningún `glassnote.exe`: el ejecutable ES el binario de
23
+ // electron que bajó npm, y la app va como argumento. Sin esto los tres autoarranques
24
+ // de abajo se salían sin hacer nada (comprueban que el binario se llame glassnote) y
25
+ // el cliente no volvía a abrirse nunca solo.
26
+ getLaunchCommand() {
27
+ if (npmMode.isNpmMode()) {
28
+ // Las banderas del sandbox van también aquí: sin ellas el arranque automático
29
+ // fallaría en las máquinas donde Chromium aborta, y el fallo no lo vería nadie
30
+ // porque nadie mira la salida de una entrada de autoarranque.
31
+ return {
32
+ exec: process.execPath,
33
+ args: [npmMode.PACKAGE_ROOT].concat(npmMode.sandboxFlags(process.execPath)),
34
+ };
35
+ }
36
+ return { exec: this.getExecutablePath(), args: [] };
37
+ }
38
+
39
+ // Get the path to the executable (app when packaged, or electron during development)
40
+ getExecutablePath() {
41
+
42
+ if (app && app.isPackaged) {
43
+ const appPath = process.execPath;
44
+ const executableName = path.basename(appPath).toLowerCase();
45
+
46
+ // Platform-specific expected names
47
+ let expectedName = '';
48
+ if (this.platform === 'win32') {
49
+ expectedName = 'glassnote.exe';
50
+ } else if (this.platform === 'darwin') {
51
+ expectedName = 'glassnote'; // macOS - sin extensión
52
+ } else if (this.platform === 'linux') {
53
+ expectedName = 'glassnote'; // Linux - sin extensión
54
+ }
55
+
56
+ // Check if the executable has the expected name for the platform
57
+ if (executableName !== expectedName) {
58
+ return appPath;
59
+ }
60
+
61
+ return appPath;
62
+ } else {
63
+ // In development, we need to start the app with electron
64
+ return process.execPath;
65
+ }
66
+ }
67
+
68
+ // Windows auto-start implementation
69
+ setupWindowsAutoStart() {
70
+ try {
71
+
72
+ // Create shortcuts in both locations
73
+ const startupPaths = [
74
+ path.join(
75
+ process.env.APPDATA,
76
+ 'Microsoft',
77
+ 'Windows',
78
+ 'Start Menu',
79
+ 'Programs',
80
+ 'Startup',
81
+ 'glassnote.lnk'
82
+ ),
83
+ path.join(
84
+ process.env.APPDATA,
85
+ 'Microsoft',
86
+ 'Windows',
87
+ 'Start Menu',
88
+ 'Programs',
89
+ 'glassnote.lnk'
90
+ )
91
+ ];
92
+
93
+ const { exec: executablePath, args: launchArgs } = this.getLaunchCommand();
94
+
95
+ // Verify the executable exists and is named glassnote.exe
96
+ if (!fs.existsSync(executablePath)) {
97
+ console.error('Executable not found:', executablePath);
98
+ console.error('Directory contents:', fs.readdirSync(path.dirname(executablePath) || '.'));
99
+ return false;
100
+ }
101
+
102
+ const executableName = path.basename(executablePath).toLowerCase();
103
+
104
+ // Only create shortcuts if the executable is named glassnote.exe
105
+ // Don't delete existing shortcuts if the name doesn't match
106
+ // (en modo npm el binario es electron.exe y el nombre no aplica)
107
+ if (!npmMode.isNpmMode() && executableName !== 'glassnote.exe') {
108
+ return true; // Return true to indicate "success" without modifying shortcuts
109
+ }
110
+
111
+ // La carpeta de trabajo: la del paquete en modo npm, la del ejecutable en nativo.
112
+ const workingDir = launchArgs.length ? launchArgs[0] : path.dirname(executablePath);
113
+
114
+ let allSuccess = true;
115
+
116
+ for (const startupPath of startupPaths) {
117
+
118
+ try {
119
+ // Remove existing shortcut if it exists
120
+ if (fs.existsSync(startupPath)) {
121
+ fs.unlinkSync(startupPath);
122
+ } else {
123
+ }
124
+ } catch (error) {
125
+ console.error('Could not remove existing shortcut (access may be restricted):', error.message);
126
+ // Continue with creation attempt even if removal failed
127
+ }
128
+
129
+ try {
130
+ // Create shortcut using Windows Script Host
131
+ const script = `
132
+ Set oWS = WScript.CreateObject("WScript.Shell")
133
+ Set oLink = oWS.CreateShortcut("${startupPath.replace(/\\/g, '\\\\')}")
134
+ oLink.TargetPath = "${executablePath.replace(/\\/g, '\\\\')}"
135
+ oLink.Arguments = "${launchArgs.map((a) => `""${a.replace(/\\/g, '\\\\')}""`).join(' ')}"
136
+ oLink.WorkingDirectory = "${workingDir.replace(/\\/g, '\\\\')}"
137
+ oLink.WindowStyle = 7
138
+ oLink.Save
139
+ `;
140
+
141
+ // Use temp directory instead of app directory (app.asar is read-only)
142
+ const tempDir = os.tmpdir();
143
+ const tempScriptPath = path.join(tempDir, 'glassnote_create_shortcut.vbs');
144
+
145
+ fs.writeFileSync(tempScriptPath, script);
146
+
147
+ execSync(`cscript //nologo "${tempScriptPath}"`);
148
+
149
+ fs.unlinkSync(tempScriptPath);
150
+
151
+ // Verify the shortcut was created
152
+ if (fs.existsSync(startupPath)) {
153
+ } else {
154
+ // Don't mark as failure if access is restricted
155
+ try {
156
+ if (fs.existsSync(path.dirname(startupPath))) {
157
+ }
158
+ } catch (dirError) {
159
+ console.error('Cannot access directory (access restricted):', dirError.message);
160
+ }
161
+ // Continue without marking as failure
162
+ }
163
+ } catch (error) {
164
+ console.error('Could not create shortcut (access may be restricted):', error.message);
165
+ // Don't mark as failure if access is restricted
166
+ }
167
+ }
168
+
169
+ if (allSuccess) {
170
+ return true;
171
+ } else {
172
+ console.error('Windows auto-start configuration partially failed');
173
+ return false;
174
+ }
175
+ } catch (error) {
176
+ console.error('Error configuring Windows auto-start:', error);
177
+ console.error('Error message:', error.message);
178
+ if (error.stdout) console.error('stdout:', error.stdout.toString());
179
+ if (error.stderr) console.error('stderr:', error.stderr.toString());
180
+ return false;
181
+ }
182
+ }
183
+
184
+ // macOS auto-start implementation
185
+ setupMacAutoStart() {
186
+ try {
187
+
188
+ const plistPath = path.join(
189
+ process.env.HOME,
190
+ 'Library',
191
+ 'LaunchAgents',
192
+ 'com.glassnote.app.plist'
193
+ );
194
+
195
+
196
+ const { exec: executablePath, args: launchArgs } = this.getLaunchCommand();
197
+
198
+ // Verify the executable exists
199
+ if (!fs.existsSync(executablePath)) {
200
+ console.error('Executable not found:', executablePath);
201
+ return false;
202
+ }
203
+
204
+ // Check if the executable is named glassnote (macOS binary name)
205
+ const executableName = path.basename(executablePath).toLowerCase();
206
+
207
+ // Instalado por npm el binario es el Electron de node_modules y no está en
208
+ // /Applications: las dos comprobaciones de abajo solo valen para el .app nativo.
209
+ if (!npmMode.isNpmMode()) {
210
+ // Only create plist if the executable is named glassnote
211
+ // Don't remove existing plist if the name doesn't match
212
+ if (executableName !== 'glassnote') {
213
+ return true; // Return true to indicate "success" without modifying plist
214
+ }
215
+
216
+ // Only create auto-start if the application is installed in the Applications folder
217
+ const applicationsPath = '/Applications/';
218
+ if (!executablePath.startsWith(applicationsPath)) {
219
+ return true; // Return true to indicate "success" without modifying plist
220
+ }
221
+ }
222
+
223
+ // Remove existing plist if it exists
224
+ if (fs.existsSync(plistPath)) {
225
+ try {
226
+ // Use spawn instead of execSync to avoid blocking
227
+ const unloadProcess = spawn('launchctl', ['unload', plistPath], {
228
+ stdio: 'ignore',
229
+ timeout: 5000 // 5 second timeout
230
+ });
231
+
232
+ unloadProcess.on('error', (error) => {
233
+ });
234
+
235
+ unloadProcess.on('exit', (code) => {
236
+ if (code !== 0) {
237
+ console.warn('launchctl unload exited with code:', code, '(may not be loaded)');
238
+ }
239
+ });
240
+ } catch (error) {
241
+ console.error('Could not unload existing plist:', error.message);
242
+ }
243
+
244
+ // Remove the plist file regardless of launchctl status
245
+ try {
246
+ fs.unlinkSync(plistPath);
247
+ } catch (error) {
248
+ console.error('Could not remove plist file:', error.message);
249
+ }
250
+ }
251
+
252
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
253
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
254
+ <plist version="1.0">
255
+ <dict>
256
+ <key>Label</key>
257
+ <string>com.glassnote.app</string>
258
+ <key>ProgramArguments</key>
259
+ <array>
260
+ <string>${executablePath}</string>
261
+ ${launchArgs.map((a) => ` <string>${a}</string>`).join('\n')}
262
+ </array>
263
+ <key>RunAtLoad</key>
264
+ <true/>
265
+ <key>KeepAlive</key>
266
+ <false/>
267
+ </dict>
268
+ </plist>`;
269
+
270
+ // Create directory if it doesn't exist
271
+ const plistDir = path.dirname(plistPath);
272
+ if (!fs.existsSync(plistDir)) {
273
+ fs.mkdirSync(plistDir, { recursive: true });
274
+ }
275
+
276
+ fs.writeFileSync(plistPath, plistContent);
277
+
278
+ // Load the launch agent asynchronously
279
+ try {
280
+ const loadProcess = spawn('launchctl', ['load', plistPath], {
281
+ stdio: 'ignore',
282
+ timeout: 5000 // 5 second timeout
283
+ });
284
+
285
+ loadProcess.on('error', (error) => {
286
+ console.error('Error spawning launchctl load:', error.message);
287
+ });
288
+
289
+ loadProcess.on('exit', (code) => {
290
+ if (code !== 0) {
291
+ console.warn('launchctl load exited with code:', code);
292
+ } else {
293
+ console.warn('macOS auto-start configured:', plistPath);
294
+ }
295
+ });
296
+ } catch (error) {
297
+ console.error('Error loading launch agent:', error.message);
298
+ }
299
+
300
+ return true;
301
+ } catch (error) {
302
+ console.error('Error configuring macOS auto-start:', error);
303
+ console.error('Error message:', error.message);
304
+ return false;
305
+ }
306
+ }
307
+
308
+ // Linux auto-start implementation
309
+ setupLinuxAutoStart() {
310
+ try {
311
+
312
+ const desktopFilePaths = [
313
+ path.join(process.env.HOME, '.local', 'share', 'applications', 'glassnote.desktop'),
314
+ path.join(process.env.HOME, '.config', 'autostart', 'glassnote.desktop')
315
+ ];
316
+
317
+ const { exec: executablePath, args: launchArgs } = this.getLaunchCommand();
318
+
319
+ // Verify the executable exists
320
+ if (!fs.existsSync(executablePath)) {
321
+ console.error('Executable not found:', executablePath);
322
+ return false;
323
+ }
324
+
325
+ // Check if the executable is named glassnote (Linux typically uses the binary name)
326
+ const executableName = path.basename(executablePath).toLowerCase();
327
+
328
+ // En modo npm el binario se llama `electron`: la comprobación de nombre solo vale
329
+ // para el empaquetado nativo.
330
+ if (!npmMode.isNpmMode() && executableName !== 'glassnote') {
331
+ return true; // Return true to indicate "success" without modifying desktop files
332
+ }
333
+
334
+ // Comillas en Exec: la ruta del paquete npm puede tener espacios.
335
+ const execLine = [executablePath].concat(launchArgs).map((a) => `"${a}"`).join(' ');
336
+ const desktopContent = `[Desktop Entry]
337
+ Type=Application
338
+ Name=Glassnote
339
+ Exec=${execLine}
340
+ Icon=${npmMode.isNpmMode() ? path.join(npmMode.PACKAGE_ROOT, 'images', 'icon.png') : 'glassnote'}
341
+ Categories=Utility;
342
+ Terminal=false
343
+ StartupWMClass=glassnote`;
344
+
345
+ for (const desktopFilePath of desktopFilePaths) {
346
+
347
+ // Remove existing desktop file if it exists
348
+ if (fs.existsSync(desktopFilePath)) {
349
+ fs.unlinkSync(desktopFilePath);
350
+ }
351
+
352
+ const desktopDir = path.dirname(desktopFilePath);
353
+ if (!fs.existsSync(desktopDir)) {
354
+ fs.mkdirSync(desktopDir, { recursive: true });
355
+ }
356
+
357
+ fs.writeFileSync(desktopFilePath, desktopContent);
358
+
359
+ // Set executable permissions
360
+ try {
361
+ execSync(`chmod +x "${desktopFilePath}"`);
362
+ } catch (error) {
363
+ console.error('Could not set executable permissions (may be ignored):', error.message);
364
+ }
365
+ }
366
+
367
+ // Update desktop database
368
+ try {
369
+ execSync('update-desktop-database ~/.local/share/applications');
370
+ } catch (error) {
371
+ console.error('Could not update desktop database (may be ignored):', error.message);
372
+ }
373
+
374
+ return true;
375
+ } catch (error) {
376
+ console.error('Error configuring Linux auto-start:', error);
377
+ console.error('Error message:', error.message);
378
+ return false;
379
+ }
380
+ }
381
+
382
+ // Check if auto-start is already configured
383
+ isAutoStartConfigured() {
384
+ try {
385
+
386
+ switch (this.platform) {
387
+ case 'win32':
388
+ try {
389
+ // Check both startup and programs menu shortcuts
390
+ const winPaths = [
391
+ path.join(
392
+ process.env.APPDATA,
393
+ 'Microsoft',
394
+ 'Windows',
395
+ 'Start Menu',
396
+ 'Programs',
397
+ 'Startup',
398
+ 'glassnote.lnk'
399
+ ),
400
+ path.join(
401
+ process.env.APPDATA,
402
+ 'Microsoft',
403
+ 'Windows',
404
+ 'Start Menu',
405
+ 'Programs',
406
+ 'glassnote.lnk'
407
+ )
408
+ ];
409
+
410
+ for (const winPath of winPaths) {
411
+ try {
412
+ if (fs.existsSync(winPath)) {
413
+ return true; // Return true if any shortcut exists
414
+ }
415
+ } catch (error) {
416
+ console.error('Cannot check Windows shortcut (access may be restricted):', winPath, error.message);
417
+ // Continue checking other paths
418
+ }
419
+ }
420
+ return false;
421
+ } catch (error) {
422
+ console.error('Cannot check Windows auto-start configuration (access may be restricted):', error.message);
423
+ return false; // Assume not configured if access is restricted
424
+ }
425
+
426
+ case 'darwin':
427
+ try {
428
+ const macPath = path.join(
429
+ process.env.HOME,
430
+ 'Library',
431
+ 'LaunchAgents',
432
+ 'com.glassnote.app.plist'
433
+ );
434
+ const macExists = fs.existsSync(macPath);
435
+ return macExists;
436
+ } catch (error) {
437
+ console.error('Cannot check macOS auto-start configuration (access may be restricted):', error.message);
438
+ return false; // Assume not configured if access is restricted
439
+ }
440
+
441
+ case 'linux':
442
+ try {
443
+ const linuxPath = path.join(
444
+ process.env.HOME,
445
+ '.config',
446
+ 'autostart',
447
+ 'glassnote.desktop'
448
+ );
449
+ const linuxExists = fs.existsSync(linuxPath);
450
+ return linuxExists;
451
+ } catch (error) {
452
+ console.error('Cannot check Linux auto-start configuration (access may be restricted):', error.message);
453
+ return false; // Assume not configured if access is restricted
454
+ }
455
+
456
+ default:
457
+ return false;
458
+ }
459
+ } catch (error) {
460
+ console.error('Error checking auto-start configuration:', error.message);
461
+ return false;
462
+ }
463
+ }
464
+
465
+ // Main method to configure auto-start
466
+ configureAutoStart() {
467
+ try {
468
+ let result = false;
469
+ switch (this.platform) {
470
+ case 'win32':
471
+ result = this.setupWindowsAutoStart();
472
+ break;
473
+
474
+ case 'darwin':
475
+ result = this.setupMacAutoStart();
476
+ break;
477
+
478
+ case 'linux':
479
+ result = this.setupLinuxAutoStart();
480
+ break;
481
+
482
+ default:
483
+ result = false;
484
+ }
485
+
486
+ if (!result) {
487
+ console.warn('Auto-start configuration may have failed due to access restrictions, but application will continue running normally');
488
+ }
489
+
490
+ return result;
491
+ } catch (error) {
492
+ console.error('Auto-start configuration failed due to error, but application will continue running normally:', error.message);
493
+ return false; // Return false but don't throw error
494
+ }
495
+ }
496
+
497
+ // Remove auto-start configuration
498
+ removeAutoStart() {
499
+ try {
500
+ switch (this.platform) {
501
+ case 'win32':
502
+ // Remove both startup and programs menu shortcuts
503
+ const winPaths = [
504
+ path.join(
505
+ process.env.APPDATA,
506
+ 'Microsoft',
507
+ 'Windows',
508
+ 'Start Menu',
509
+ 'Programs',
510
+ 'Startup',
511
+ 'glassnote.lnk'
512
+ ),
513
+ path.join(
514
+ process.env.APPDATA,
515
+ 'Microsoft',
516
+ 'Windows',
517
+ 'Start Menu',
518
+ 'Programs',
519
+ 'glassnote.lnk'
520
+ )
521
+ ];
522
+
523
+ for (const winPath of winPaths) {
524
+ try {
525
+ if (fs.existsSync(winPath)) {
526
+ fs.unlinkSync(winPath);
527
+ } else {
528
+ }
529
+ } catch (error) {
530
+ console.error('Could not remove Windows shortcut (access may be restricted):', winPath, error.message);
531
+ // Continue without error - removal is optional
532
+ }
533
+ }
534
+ break;
535
+
536
+ case 'darwin':
537
+ try {
538
+ const macPath = path.join(
539
+ process.env.HOME,
540
+ 'Library',
541
+ 'LaunchAgents',
542
+ 'com.glassnote.app.plist'
543
+ );
544
+ if (fs.existsSync(macPath)) {
545
+ try {
546
+ // Use spawn instead of execSync to avoid blocking
547
+ const unloadProcess = spawn('launchctl', ['unload', macPath], {
548
+ stdio: 'ignore',
549
+ timeout: 5000 // 5 second timeout
550
+ });
551
+
552
+ unloadProcess.on('error', (error) => {
553
+ console.error('Error spawning launchctl unload:', error.message);
554
+ });
555
+
556
+ unloadProcess.on('exit', (code) => {
557
+ if (code !== 0) {
558
+ console.warn('launchctl unload exited with code:', code, '(may not be loaded)');
559
+ }
560
+ });
561
+ } catch (error) {
562
+ console.error('Could not unload plist:', error.message);
563
+ }
564
+
565
+ // Remove the plist file regardless of launchctl status
566
+ try {
567
+ fs.unlinkSync(macPath);
568
+ } catch (error) {
569
+ console.error('Could not remove plist file:', error.message);
570
+ }
571
+ } else {
572
+ console.warn('macOS auto-start plist not found');
573
+ }
574
+ } catch (error) {
575
+ console.error('Could not remove macOS auto-start configuration (access may be restricted):', error.message);
576
+ // Continue without error - removal is optional
577
+ }
578
+ break;
579
+
580
+ case 'linux':
581
+ try {
582
+ const linuxPaths = [
583
+ path.join(process.env.HOME, '.local', 'share', 'applications', 'glassnote.desktop'),
584
+ path.join(process.env.HOME, '.config', 'autostart', 'glassnote.desktop')
585
+ ];
586
+ for (const linuxPath of linuxPaths) {
587
+ if (fs.existsSync(linuxPath)) {
588
+ fs.unlinkSync(linuxPath);
589
+ } else {
590
+ console.warn('Linux desktop file not found:', linuxPath);
591
+ }
592
+ }
593
+ } catch (error) {
594
+ console.error('Could not remove Linux auto-start configuration (access may be restricted):', error.message);
595
+ // Continue without error - removal is optional
596
+ }
597
+ break;
598
+ }
599
+
600
+ return true; // Always return true since removal is optional
601
+ } catch (error) {
602
+ console.error('Error removing auto-start configuration:', error.message);
603
+ return true; // Return true even if there's an error - removal is optional
604
+ }
605
+ }
606
+ }
607
+
608
+ module.exports = AutoStart;