@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.
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env node
2
+ // glassnote — instalador y lanzador del cliente por npm/npx.
3
+ //
4
+ // npx @glassnote/client corre el cliente una vez (nada queda instalado)
5
+ // npx @glassnote/client install lo instala de verdad y deja el comando `glassnote`
6
+ // glassnote update actualiza a la última publicada
7
+ // glassnote uninstall quita autoarranque, ~/.glassnote y la línea del rc
8
+ //
9
+ // Sin instaladores nativos, sin firmas y sin tiendas: npm baja el binario de electron
10
+ // por su cuenta (no pasa por el navegador, así que macOS no le pone quarantine y
11
+ // Gatekeeper no lo bloquea) y el cliente se ejecuta con ese electron.
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const { spawn, spawnSync } = require('child_process');
18
+
19
+ const mode = require('../npmMode');
20
+ const pkg = require('../package.json');
21
+
22
+ const IS_WIN = process.platform === 'win32';
23
+ const NPM_CMD = IS_WIN ? 'npm.cmd' : 'npm';
24
+ const MARK_A = '# >>> glassnote >>>';
25
+ const MARK_B = '# <<< glassnote <<<';
26
+
27
+ // El PATH que se le da a npm lleva delante nuestro bin: si install.sh tuvo que bajar
28
+ // Node, sus enlaces viven ahí y de otro modo npm no existiría para este proceso.
29
+ function npmEnv() {
30
+ return Object.assign({}, process.env, {
31
+ PATH: `${mode.BIN_DIR}${path.delimiter}${process.env.PATH || ''}`,
32
+ });
33
+ }
34
+
35
+ // Sueño síncrono sin dependencias: el ayudante de actualización tiene que esperar a que
36
+ // la app termine de cerrarse antes de que npm le reemplace los archivos.
37
+ function sleepSync(ms) {
38
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
39
+ }
40
+
41
+ const log = (...args) => console.log(...args);
42
+ // Cada paso se dice EN CUANTO EMPIEZA: lo que hace que un instalador parezca colgado
43
+ // no es que tarde, es que tarde sin decir en qué está.
44
+ const step = (msg) => console.log(` · ${msg}`);
45
+ const fail = (msg) => {
46
+ console.error(`glassnote: ${msg}`);
47
+ process.exit(1);
48
+ };
49
+
50
+ // ── electron ────────────────────────────────────────────────────────────────────────
51
+ function electronPath() {
52
+ let binary;
53
+ try {
54
+ binary = require('electron');
55
+ } catch (error) {
56
+ fail(
57
+ 'no encuentro electron.\n' +
58
+ ' Si instalaste con --ignore-scripts, el binario no se descargó. Reinstalá sin esa bandera:\n' +
59
+ ` npx ${mode.PKG_NAME} install`
60
+ );
61
+ }
62
+ if (typeof binary !== 'string' || !fs.existsSync(binary)) {
63
+ fail('electron está instalado pero su binario no se descargó. Reinstalá el paquete.');
64
+ }
65
+ return binary;
66
+ }
67
+
68
+ function launchArgs(binary) {
69
+ const flags = mode.sandboxFlags(binary);
70
+ if (flags.includes('--no-sandbox') && !warnedNoSandbox) {
71
+ warnedNoSandbox = true;
72
+ console.error(
73
+ 'glassnote: arrancando SIN sandbox de Chromium (esta máquina no permite otra cosa\n' +
74
+ ' para una instalación sin root). Para recuperarlo, una sola vez: glassnote fix-sandbox'
75
+ );
76
+ }
77
+ return [mode.PACKAGE_ROOT].concat(flags);
78
+ }
79
+
80
+ let warnedNoSandbox = false;
81
+
82
+ function launch({ detach = false, extra = [] } = {}) {
83
+ const binary = electronPath();
84
+ const args = launchArgs(binary).concat(extra);
85
+ const env = Object.assign({}, process.env, {
86
+ // El modo se marca aquí, no se adivina desde el main: sobrevive a symlinks.
87
+ GLASSNOTE_INSTALL_MODE: mode.detect(),
88
+ GLASSNOTE_CLI: path.join(__dirname, 'glassnote.js'),
89
+ });
90
+ // Si veníamos corriendo como node dentro de electron, hay que quitarlo o el cliente
91
+ // arrancaría otra vez como node y no abriría ninguna ventana.
92
+ delete env.ELECTRON_RUN_AS_NODE;
93
+
94
+ if (mode.isEphemeral()) {
95
+ log('Corriendo por npx: nada queda instalado y no se registra el arranque automático.');
96
+ log(`Para instalarlo de verdad: npx ${mode.PKG_NAME} install`);
97
+ }
98
+
99
+ if (detach) {
100
+ const child = spawn(binary, args, { detached: true, stdio: 'ignore', env });
101
+ child.unref();
102
+ return 0;
103
+ }
104
+
105
+ const child = spawnSync(binary, args, { stdio: 'inherit', env });
106
+ if (child.error) fail(`no se pudo arrancar el cliente: ${child.error.message}`);
107
+ if (child.status !== 0 && process.platform === 'linux') {
108
+ console.error(
109
+ '\nSi falló por librerías del sistema, en Debian/Ubuntu suelen faltar:\n' +
110
+ ' sudo apt-get install -y libgtk-3-0 libnss3 libasound2 libxss1 libgbm1\n' +
111
+ 'En un servidor sin escritorio el cliente no puede correr: necesita sesión gráfica.'
112
+ );
113
+ }
114
+ return child.status === null ? 1 : child.status;
115
+ }
116
+
117
+ // ── PATH ────────────────────────────────────────────────────────────────────────────
118
+ function addToPathUnix() {
119
+ const line = `export PATH="${mode.BIN_DIR}:$PATH"`;
120
+ // Los rc del shell interactivo primero. `.profile` SOLO si no hay ninguno: escribir
121
+ // en los dos deja el mismo directorio dos veces en el PATH.
122
+ let rcs = [path.join(os.homedir(), '.bashrc'), path.join(os.homedir(), '.zshrc')].filter((rc) =>
123
+ fs.existsSync(rc)
124
+ );
125
+ if (rcs.length === 0) rcs = [path.join(os.homedir(), '.profile')];
126
+
127
+ const touched = [];
128
+ for (const rc of rcs) {
129
+ let content = '';
130
+ try {
131
+ content = fs.readFileSync(rc, 'utf8');
132
+ } catch (error) {
133
+ content = '';
134
+ }
135
+ if (content.includes(MARK_A)) continue; // idempotente
136
+ fs.appendFileSync(rc, `\n${MARK_A}\n${line}\n${MARK_B}\n`);
137
+ touched.push(rc);
138
+ }
139
+ return { touched, line };
140
+ }
141
+
142
+ function addToPathWindows() {
143
+ // Nada de `setx`: trunca el PATH a 1024 caracteres y se come el de la gente.
144
+ const ps = [
145
+ "$dir = '" + mode.BIN_DIR.replace(/'/g, "''") + "'",
146
+ "$cur = [Environment]::GetEnvironmentVariable('Path','User')",
147
+ "if ($null -eq $cur) { $cur = '' }",
148
+ "if (($cur -split ';') -notcontains $dir) {",
149
+ " $new = if ($cur.Trim() -eq '') { $dir } else { $cur.TrimEnd(';') + ';' + $dir }",
150
+ " [Environment]::SetEnvironmentVariable('Path', $new, 'User')",
151
+ " Write-Output 'added'",
152
+ '} else { Write-Output "present" }',
153
+ ].join('; ');
154
+ const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
155
+ encoding: 'utf8',
156
+ });
157
+ const added = res.status === 0 && String(res.stdout).includes('added');
158
+ return { touched: added ? ['PATH de usuario (sin administrador)'] : [], line: mode.BIN_DIR };
159
+ }
160
+
161
+ function removeFromPathUnix() {
162
+ const rcs = [
163
+ path.join(os.homedir(), '.bashrc'),
164
+ path.join(os.homedir(), '.zshrc'),
165
+ path.join(os.homedir(), '.profile'),
166
+ ];
167
+ const touched = [];
168
+ for (const rc of rcs) {
169
+ if (!fs.existsSync(rc)) continue;
170
+ const content = fs.readFileSync(rc, 'utf8');
171
+ if (!content.includes(MARK_A)) continue;
172
+ // Solo se borra lo que está entre NUESTROS marcadores; el resto no se toca.
173
+ const cleaned = content.replace(
174
+ new RegExp(`\\n?${MARK_A}[\\s\\S]*?${MARK_B}\\n?`, 'g'),
175
+ '\n'
176
+ );
177
+ fs.writeFileSync(rc, cleaned);
178
+ touched.push(rc);
179
+ }
180
+ return touched;
181
+ }
182
+
183
+ function removeFromPathWindows() {
184
+ // Simétrico a addToPathWindows: si install lo agrega con SetEnvironmentVariable,
185
+ // uninstall lo quita igual. Decirle al usuario que lo borre a mano dejaba una
186
+ // entrada muerta en el PATH de todo el mundo que desinstalara.
187
+ const ps = [
188
+ "$dir = '" + mode.BIN_DIR.replace(/'/g, "''") + "'",
189
+ "$cur = [Environment]::GetEnvironmentVariable('Path','User')",
190
+ "if ($null -eq $cur) { Write-Output 'absent'; exit }",
191
+ "$new = (($cur -split ';') | Where-Object { $_ -ne '' -and $_ -ne $dir }) -join ';'",
192
+ 'if ($new -ne $cur) {',
193
+ " [Environment]::SetEnvironmentVariable('Path', $new, 'User')",
194
+ " Write-Output 'removed'",
195
+ "} else { Write-Output 'absent' }",
196
+ ].join('; ');
197
+ const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], {
198
+ encoding: 'utf8',
199
+ });
200
+ return res.status === 0 && String(res.stdout).includes('removed');
201
+ }
202
+
203
+ // ── autoarranque ────────────────────────────────────────────────────────────────────
204
+ // autoStart.js lo escribe la app al arrancar; para quitarlo desde el CLI se reusa la
205
+ // misma clase (carga electron de forma perezosa, así funciona bajo node puro).
206
+ function removeAutoStart() {
207
+ try {
208
+ const AutoStart = require('../autoStart');
209
+ new AutoStart().removeAutoStart();
210
+ return true;
211
+ } catch (error) {
212
+ console.error(` no se pudo limpiar el autoarranque: ${error.message}`);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ // ── comandos ────────────────────────────────────────────────────────────────────────
218
+ // `glassnote install` acepta una versión (`2.3.0`, `latest`) o directamente un spec de
219
+ // npm: un .tgz, una ruta local o una URL. Lo segundo es lo que permite probar un paquete
220
+ // antes de publicarlo y lo que hace falta si algún día esto vive en un registry privado.
221
+ // Del DISCO, no con require(): cuando el paquete se actualiza a sí mismo, este proceso
222
+ // ya tiene cargado su propio package.json y require() lo devolvería de la caché — un
223
+ // update correcto informaba la versión ANTERIOR y parecía fallido.
224
+ function installedVersion() {
225
+ try {
226
+ const file = path.join(mode.INSTALLED_PACKAGE_DIR, 'package.json');
227
+ return JSON.parse(fs.readFileSync(file, 'utf8')).version;
228
+ } catch (error) {
229
+ return 'desconocida';
230
+ }
231
+ }
232
+
233
+ function toSpec(arg) {
234
+ if (!arg || arg === 'latest') return `${mode.PKG_NAME}@latest`;
235
+ const looksLikeSpec =
236
+ arg.endsWith('.tgz') ||
237
+ arg.startsWith('.') ||
238
+ arg.startsWith('/') ||
239
+ arg.startsWith('file:') ||
240
+ arg.includes('://') ||
241
+ arg.startsWith('@');
242
+ return looksLikeSpec ? arg : `${mode.PKG_NAME}@${arg}`;
243
+ }
244
+
245
+ function cmdInstall(args) {
246
+ const version = args.find((a) => !a.startsWith('-')) || 'latest';
247
+ const noPath = args.includes('--no-path');
248
+ const noStart = args.includes('--no-start');
249
+
250
+ const spec = toSpec(version);
251
+ log(`glassnote — instalando ${spec}`);
252
+ fs.mkdirSync(mode.NPM_PREFIX, { recursive: true });
253
+ fs.mkdirSync(mode.BIN_DIR, { recursive: true });
254
+
255
+ // Los scripts de instalación van ACTIVADOS a propósito: el binario de electron se
256
+ // baja en un postinstall. Con --ignore-scripts quedaría instalado y roto.
257
+ step(`npm install -g --prefix ${mode.NPM_PREFIX} (sin root, la primera vez tarda: baja electron)`);
258
+ const res = spawnSync(NPM_CMD, ['install', '-g', '--prefix', mode.NPM_PREFIX, spec], {
259
+ stdio: 'inherit',
260
+ shell: IS_WIN,
261
+ env: npmEnv(),
262
+ });
263
+ if (res.status !== 0) fail('npm no pudo instalar el paquete (ver el error de arriba).');
264
+
265
+ if (!fs.existsSync(mode.INSTALLED_PACKAGE_DIR)) {
266
+ fail(`npm terminó pero no encuentro ${mode.INSTALLED_PACKAGE_DIR}`);
267
+ }
268
+
269
+ // Un solo directorio en el PATH, con enlaces a lo instalado.
270
+ if (!IS_WIN) {
271
+ const target = path.join(mode.NPM_PREFIX, 'bin', mode.BIN_NAME);
272
+ const link = path.join(mode.BIN_DIR, mode.BIN_NAME);
273
+ if (fs.existsSync(target)) {
274
+ step(`enlazando ${mode.BIN_NAME} en ${mode.BIN_DIR}`);
275
+ try {
276
+ fs.unlinkSync(link);
277
+ } catch (error) {
278
+ /* no existía */
279
+ }
280
+ fs.symlinkSync(target, link);
281
+ }
282
+ }
283
+
284
+ let pathInfo = { touched: [], line: '' };
285
+ if (!noPath) {
286
+ pathInfo = IS_WIN ? addToPathWindows() : addToPathUnix();
287
+ if (pathInfo.touched.length) step(`agregado al PATH en: ${pathInfo.touched.join(' ')}`);
288
+ }
289
+
290
+ log('');
291
+ log(`Instalado: ${mode.PKG_NAME} v${installedVersion()} en ${mode.HOME_DIR}`);
292
+ if (pathInfo.touched.length) {
293
+ log(`Una terminal NUEVA ya lo encuentra. En esta:`);
294
+ log(IS_WIN ? ` set PATH=${mode.BIN_DIR};%PATH%` : ` export PATH="${mode.BIN_DIR}:$PATH"`);
295
+ } else if (!noPath) {
296
+ log('Tu PATH ya lo tenía.');
297
+ } else {
298
+ log('No toqué tu PATH. Agregá esta línea a la config de tu shell:');
299
+ log(IS_WIN ? ` ${mode.BIN_DIR}` : ` export PATH="${mode.BIN_DIR}:$PATH"`);
300
+ }
301
+ log('Para actualizar más adelante: glassnote update (o correr esto mismo otra vez).');
302
+ log('');
303
+
304
+ if (noStart) {
305
+ log('Arrancalo cuando quieras: glassnote');
306
+ return 0;
307
+ }
308
+
309
+ // Se arranca desprendido: es una app de bandeja, no una herramienta de terminal.
310
+ // Al arrancar, la app se registra sola en el autoarranque del usuario.
311
+ step('arrancando el cliente en segundo plano (queda en la bandeja del sistema)');
312
+ const binary = path.join(mode.INSTALLED_PACKAGE_DIR, 'bin', 'glassnote.js');
313
+ const child = spawn(process.execPath, [binary, 'start', '--detach'], {
314
+ detached: true,
315
+ stdio: 'ignore',
316
+ env: process.env,
317
+ });
318
+ child.unref();
319
+ log('Listo. Ícono en la bandeja; se abrirá solo en los próximos arranques.');
320
+ return 0;
321
+ }
322
+
323
+ function cmdUpdate(args) {
324
+ if (mode.isEphemeral()) {
325
+ log('Estás corriendo por npx: npx ya baja la última versión cada vez.');
326
+ log(`Para dejarlo instalado: npx ${mode.PKG_NAME} install`);
327
+ return 0;
328
+ }
329
+
330
+ // `--wait` y `--relaunch` los usa la app al auto-actualizarse: espera a que el
331
+ // cliente termine de cerrarse (en Windows npm no puede tocar un .exe en marcha) y
332
+ // lo vuelve a abrir al terminar.
333
+ const waitIdx = args.indexOf('--wait');
334
+ const waitMs = waitIdx !== -1 ? parseInt(args[waitIdx + 1], 10) || 0 : 0;
335
+ const relaunch = args.includes('--relaunch');
336
+ const positional = args.filter((a, i) => !a.startsWith('-') && i !== waitIdx + 1);
337
+ const version = positional[0] || 'latest';
338
+
339
+ if (waitMs > 0) sleepSync(waitMs);
340
+
341
+ const spec = toSpec(version);
342
+ log(`glassnote — actualizando a ${spec}`);
343
+ const res = spawnSync(NPM_CMD, ['install', '-g', '--prefix', mode.NPM_PREFIX, spec], {
344
+ stdio: 'inherit',
345
+ shell: IS_WIN,
346
+ env: npmEnv(),
347
+ });
348
+ if (res.status !== 0) {
349
+ if (relaunch) {
350
+ // Falló la actualización: al menos el cliente vuelve. Quedarse sin app por
351
+ // un npm que no pudo con la red es peor que seguir en la versión vieja.
352
+ console.error('glassnote: la actualización falló; reabriendo la versión instalada.');
353
+ launch({ detach: true });
354
+ }
355
+ fail('npm no pudo actualizar el paquete.');
356
+ }
357
+
358
+ log(`Actualizado a v${installedVersion()}.`);
359
+ if (relaunch) {
360
+ launch({ detach: true });
361
+ return 0;
362
+ }
363
+ log('Reiniciá el cliente para aplicarlo.');
364
+ return 0;
365
+ }
366
+
367
+ function cmdUninstall() {
368
+ log('glassnote — desinstalando');
369
+ step('quitando el autoarranque');
370
+ removeAutoStart();
371
+
372
+ if (!IS_WIN) {
373
+ const touched = removeFromPathUnix();
374
+ if (touched.length) step(`línea del PATH quitada de: ${touched.join(' ')}`);
375
+ } else if (removeFromPathWindows()) {
376
+ step('quitado del PATH de usuario (las terminales ya abiertas siguen con el viejo)');
377
+ } else {
378
+ step('el PATH de usuario ya no tenía la entrada');
379
+ }
380
+
381
+ if (fs.existsSync(mode.HOME_DIR)) {
382
+ step(`borrando ${mode.HOME_DIR}`);
383
+ fs.rmSync(mode.HOME_DIR, { recursive: true, force: true });
384
+ }
385
+ log('');
386
+ log('Desinstalado. Tus datos y notas siguen donde estaban:');
387
+ log(` ${userDataDir()}`);
388
+ log('Borralos a mano si también querés perderlos.');
389
+ return 0;
390
+ }
391
+
392
+ function userDataDir() {
393
+ const base =
394
+ process.env.APPDATA ||
395
+ (process.platform === 'darwin'
396
+ ? path.join(os.homedir(), 'Library', 'Application Support')
397
+ : path.join(os.homedir(), '.config'));
398
+ return path.join(base, 'glassnote');
399
+ }
400
+
401
+ // Deja chrome-sandbox como Chromium lo quiere: root:root y setuid. Es lo ÚNICO que
402
+ // necesita permisos de administrador en toda la instalación, es opcional, y se pide
403
+ // explícitamente — no a espaldas del usuario durante un `install`.
404
+ function cmdFixSandbox() {
405
+ if (process.platform !== 'linux') {
406
+ log('Solo hace falta en Linux; en Windows y macOS el sandbox ya funciona.');
407
+ return 0;
408
+ }
409
+ const binary = electronPath();
410
+ const sandbox = mode.chromeSandboxPath(binary);
411
+ if (!fs.existsSync(sandbox)) fail(`no encuentro ${sandbox}`);
412
+ if (mode.sandboxIsSetuidRoot(binary)) {
413
+ log('Ya está bien configurado: chrome-sandbox es setuid root.');
414
+ return 0;
415
+ }
416
+ log(`Hace falta sudo una vez para: chown root:root && chmod 4755 ${sandbox}`);
417
+ const res = spawnSync(
418
+ 'sudo',
419
+ ['sh', '-c', `chown root:root "${sandbox}" && chmod 4755 "${sandbox}"`],
420
+ { stdio: 'inherit' }
421
+ );
422
+ if (res.status !== 0) fail('no se pudo cambiar el permiso (¿sudo cancelado?).');
423
+ log('Listo. El cliente ya arranca con el sandbox de Chromium.');
424
+ log('Ojo: hay que repetirlo tras cada actualización, porque npm reinstala el binario.');
425
+ return 0;
426
+ }
427
+
428
+ function cmdStatus() {
429
+ let electronVersion = 'no instalado';
430
+ try {
431
+ electronVersion = require('electron/package.json').version;
432
+ } catch (error) {
433
+ /* sin electron */
434
+ }
435
+ let autoStartOn = 'desconocido';
436
+ try {
437
+ autoStartOn = new (require('../autoStart'))().isAutoStartConfigured() ? 'sí' : 'no';
438
+ } catch (error) {
439
+ /* ignore */
440
+ }
441
+ log(`glassnote v${pkg.version}`);
442
+ log(` modo: ${mode.detect()}`);
443
+ log(` paquete: ${mode.PACKAGE_ROOT}`);
444
+ log(` electron: ${electronVersion}`);
445
+ log(` home: ${mode.HOME_DIR}`);
446
+ log(` datos: ${userDataDir()}`);
447
+ log(` autoarranque: ${autoStartOn}`);
448
+ if (process.platform === 'linux') {
449
+ let sandboxState = 'sin electron';
450
+ try {
451
+ const binary = require('electron');
452
+ const flags = mode.sandboxFlags(binary);
453
+ sandboxState = flags.length === 0 ? 'setuid root (completo)' : `degradado (${flags.join(' ')})`;
454
+ } catch (error) {
455
+ /* sin electron */
456
+ }
457
+ log(` sandbox: ${sandboxState}`);
458
+ }
459
+ return 0;
460
+ }
461
+
462
+ function cmdHelp() {
463
+ log(`glassnote v${pkg.version} — cliente de escritorio
464
+
465
+ npx @glassnote/client corre el cliente sin instalar nada
466
+ npx @glassnote/client install lo instala bajo ~/.glassnote y deja el comando
467
+ glassnote arranca el cliente
468
+ glassnote start --detach lo arranca y libera la terminal
469
+ glassnote update [version] actualiza (por defecto, la última)
470
+ glassnote uninstall quita autoarranque, ~/.glassnote y la línea del rc
471
+ glassnote status versión, modo, rutas y estado del autoarranque
472
+ glassnote fix-sandbox (Linux) restaura el sandbox de Chromium; pide sudo una vez
473
+
474
+ Banderas de install:
475
+ --no-path no toca el rc; imprime la línea para pegarla
476
+ --no-start instala y no arranca el cliente
477
+ `);
478
+ return 0;
479
+ }
480
+
481
+ function main() {
482
+ const argv = process.argv.slice(2);
483
+ const cmd = argv[0];
484
+ const rest = argv.slice(1);
485
+
486
+ if (cmd === '--version' || cmd === '-v') {
487
+ log(pkg.version);
488
+ return 0;
489
+ }
490
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') return cmdHelp();
491
+
492
+ switch (cmd) {
493
+ case undefined:
494
+ case 'start':
495
+ return launch({
496
+ detach: rest.includes('--detach') || rest.includes('-d'),
497
+ extra: rest.filter((a) => a !== '--detach' && a !== '-d'),
498
+ });
499
+ case 'install':
500
+ return cmdInstall(rest);
501
+ case 'update':
502
+ case 'upgrade':
503
+ return cmdUpdate(rest);
504
+ case 'uninstall':
505
+ case 'remove':
506
+ return cmdUninstall();
507
+ case 'status':
508
+ case 'doctor':
509
+ return cmdStatus();
510
+ case 'fix-sandbox':
511
+ return cmdFixSandbox();
512
+ default:
513
+ if (cmd.startsWith('-')) {
514
+ // Banderas sueltas van al cliente: `glassnote --no-sandbox`
515
+ return launch({ extra: argv });
516
+ }
517
+ console.error(`glassnote: comando desconocido "${cmd}"\n`);
518
+ cmdHelp();
519
+ return 2;
520
+ }
521
+ }
522
+
523
+ process.exit(main());
Binary file
Binary file