@danhachuel/leadmaster 0.1.8 → 0.1.9

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/CHANGELOG.md CHANGED
@@ -6,6 +6,32 @@ O formato é baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.
6
6
  A etiqueta visual de versão usa patch de dois dígitos (ex.: `0.1.00`); o registry NPM
7
7
  normaliza para a forma canônica (ex.: `0.1.0`).
8
8
 
9
+ ## [0.1.09] — 2026-11-08
10
+
11
+ ### Corrigido
12
+
13
+ - **Encerramento silencioso do launcher no MobaXterm (Windows)** — o app morria
14
+ sozinho por sinais espúrios (perda de foco/minimização) e por EPIPE ao fechar o
15
+ terminal. Correções no `scripts/cli.mjs`:
16
+ - **Keep-alive:** `setInterval(() => {}, 1000);` mantém o event loop ativo mesmo
17
+ sem I/O de console.
18
+ - **Absorção de sinais no Windows:** `SIGINT`/`SIGHUP` agora são apenas logados
19
+ (`[LAUNCHER] Sinal absorvido no Windows`) e **não** encerram o launcher;
20
+ `SIGTERM`/`SIGBREAK` (Ctrl+Break) seguem como paradas legítimas. No
21
+ Linux/macOS o comportamento gracioso é mantido.
22
+ - **Guarda da consola (anti-EPIPE):** `guardConsoleStream()` ignora erros `EPIPE`
23
+ no `'error'` dos streams e espelha toda a saída em
24
+ `storage/logs/launcher_console.log`; escrita original com exceção retorna
25
+ silenciosamente. Aplicada a `process.stdout`/`process.stderr` antes de
26
+ qualquer escrita do launcher.
27
+ - Isolamento mantido: worker Python com `detached: true` + `windowsHide: true`
28
+ (`CREATE_NEW_PROCESS_GROUP`).
29
+ - **AGENTS.md:** nova seção *🚨 REGRA INVIOLÁVEL: ARQUITETURA DO LAUNCHER* com as
30
+ 4 proteções obrigatórias.
31
+ - **Gate de regressão:** `backend/tests/test_launcher_protections.py` falha se
32
+ keep-alive, absorção de sinais, `guardConsoleStream` ou isolamento forem
33
+ removidos do launcher.
34
+
9
35
  ## [0.1.08] — 2026-11-08
10
36
 
11
37
  ### Adicionado
@@ -0,0 +1,46 @@
1
+ """Gate de regressão da arquitetura do launcher (scripts/cli.mjs).
2
+
3
+ Lê o launcher e falha se qualquer proteção contra encerramento silencioso no
4
+ Windows (MobaXterm) for removida:
5
+
6
+ 1. Keep-alive com setInterval (event loop ativo sem I/O de console);
7
+ 2. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
8
+ 3. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
9
+ 4. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ CAMINHO = Path(__file__).resolve().parents[2] / "scripts" / "cli.mjs"
15
+
16
+
17
+ def _conteudo() -> str:
18
+ assert CAMINHO.exists(), f"launcher não encontrado em {CAMINHO}"
19
+ return CAMINHO.read_text(encoding="utf-8")
20
+
21
+
22
+ class TestLauncherProtections:
23
+ def test_keepalivo(self):
24
+ assert "setInterval(() => {}, 1000)" in _conteudo()
25
+
26
+ def test_absorcao_de_sinais_no_windows(self):
27
+ c = _conteudo()
28
+ assert "SIGINT" in c and "SIGTERM" in c and "SIGHUP" in c
29
+ assert "absorvido" in c
30
+
31
+ def test_guarda_anti_epipe(self):
32
+ c = _conteudo()
33
+ assert "guardConsoleStream" in c
34
+ assert "EPIPE" in c
35
+ assert "launcher_console.log" in c
36
+
37
+ def test_isolamento_do_worker(self):
38
+ c = _conteudo()
39
+ assert "detached: true" in c
40
+ assert "windowsHide: true" in c
41
+
42
+ def test_sigterm_nao_absorvido(self):
43
+ # SIGTERM/SIGBREAK são paradas legítimas: sempre encerram.
44
+ c = _conteudo()
45
+ assert "SIGTERM" in c and "SIGBREAK" in c
46
+ assert "naoAbsorver" in c
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/leadmaster",
3
- "version": "0.1.08",
3
+ "version": "0.1.09",
4
4
  "description": "SaaS local de prospecção e automação: CRM, extratores de leads e agentes de IA — 100% local.",
5
5
  "license": "MIT",
6
6
  "author": "Dan Hachuel",
package/scripts/cli.mjs CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import { spawn, spawnSync } from 'node:child_process';
26
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
26
+ import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs';
27
27
  import { homedir, platform } from 'node:os';
28
28
  import net from 'node:net';
29
29
  import path from 'node:path';
@@ -282,6 +282,36 @@ async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
282
282
  return null;
283
283
  }
284
284
 
285
+ /**
286
+ * Guarda da consola (anti-EPIPE): se o terminal for fechado (MobaXterm, VS
287
+ * Code), escrever em stdout/stderr gera EPIPE — que o Node trataria como
288
+ * uncaughtException, matando o launcher. Esta guarda:
289
+ * - ignora erros com code EPIPE no evento 'error' do stream;
290
+ * - espelha toda a saída em storage/logs/launcher_console.log;
291
+ * - se a escrita original falhar, retorna true silenciosamente.
292
+ * NUNCA REMOVER (regra inviolável do AGENTS.md).
293
+ */
294
+ function guardConsoleStream(stream, logFile) {
295
+ stream.on('error', (err) => {
296
+ if (err && err.code === 'EPIPE') return; // terminal fechado: ignora
297
+ return; // demais erros de consola nunca devem derrubar o launcher
298
+ });
299
+ const escritaOriginal = stream.write.bind(stream);
300
+ stream.write = (chunk, encoding, callback) => {
301
+ try {
302
+ appendFileSync(logFile, typeof chunk === 'string' ? chunk : Buffer.from(chunk));
303
+ } catch {
304
+ /* espelho de log é best-effort */
305
+ }
306
+ try {
307
+ return escritaOriginal(chunk, encoding, callback);
308
+ } catch {
309
+ return true; // escrita silenciosa sem exceção
310
+ }
311
+ };
312
+ return stream;
313
+ }
314
+
285
315
  // ---------------------------------------------------------------------------
286
316
  // Comandos
287
317
  // ---------------------------------------------------------------------------
@@ -416,9 +446,6 @@ function cmdStart(args) {
416
446
  child.kill('SIGTERM');
417
447
  }
418
448
  };
419
- process.on('SIGINT', () => { stop(); process.exit(0); });
420
- process.on('SIGTERM', () => { stop(); process.exit(0); });
421
- if (platform() === 'win32') process.on('SIGHUP', () => { stop(); process.exit(0); });
422
449
 
423
450
  console.log(`→ Subindo servidor local em ${url} ...`);
424
451
  const health = await waitForHealth(porta, 120000);
@@ -428,11 +455,39 @@ function cmdStart(args) {
428
455
  stop();
429
456
  process.exit(1);
430
457
  }
458
+
459
+ // -------------------------------------------------------------------------
460
+ // Sinais (regra inviolável do AGENTS.md)
461
+ // MobaXterm envia SIGINT/SIGHUP espúrios (perda de foco, minimização): no
462
+ // Windows esses sinais são APENAS ABSORVIDOS (log), sem encerrar o launcher,
463
+ // para o app não morrer sozinho. SIGTERM e SIGBREAK são paradas legítimas e
464
+ // sempre encerram. No Linux/macOS, SIGINT/SIGHUP mantêm o stop gracioso.
465
+ // -------------------------------------------------------------------------
466
+ const ehWindows = platform() === 'win32';
467
+ const naoAbsorver = () => { stop(); process.exit(0); };
468
+ const absorverWindows = (nome) => () => {
469
+ console.log(`[LAUNCHER] Sinal absorvido no Windows (${nome}) — o LeadMaster continua rodando.`);
470
+ };
471
+ process.on('SIGINT', ehWindows ? absorverWindows('SIGINT') : naoAbsorver);
472
+ process.on('SIGHUP', ehWindows ? absorverWindows('SIGHUP') : naoAbsorver);
473
+ process.on('SIGTERM', naoAbsorver);
474
+ if (ehWindows) {
475
+ // SIGBREAK (Ctrl+Break): parada legítima também no Windows.
476
+ process.on('SIGBREAK', naoAbsorver);
477
+ }
478
+
479
+ // Keep-alive: mantém o event loop ativo mesmo sem I/O de console (MobaXterm
480
+ // minimizado / sem stream ativo) — evita o encerramento silencioso.
481
+ setInterval(() => {}, 1000);
482
+
431
483
  console.log(`✓ LeadMaster no ar em ${url} (dados: ${home})`);
432
484
  console.log('');
433
485
  console.log(`ℹ Abra ${url} no navegador quando quiser — o LeadMaster nunca abre janelas sozinho.`);
434
- console.log('');
435
- console.log('Pressione Ctrl+C para encerrar.');
486
+ console.log(
487
+ ehWindows
488
+ ? 'ℹ Para encerrar: Ctrl+Break ou feche a janela (Ctrl+C é absorvido no Windows contra sinais espúrios do MobaXterm).'
489
+ : 'ℹ Para encerrar: Ctrl+C.',
490
+ );
436
491
  })().catch((err) => {
437
492
  console.error('✗ Erro inesperado ao iniciar:', err && err.message ? err.message : err);
438
493
  process.exit(1);
@@ -498,6 +553,15 @@ const rawArgs = process.argv.slice(2);
498
553
  const args = parseArgs(rawArgs);
499
554
  const command = args._[0] || 'start';
500
555
 
556
+ // Guarda da consola (anti-EPIPE) aplicada ANTES de qualquer escrita do
557
+ // launcher: espelha stdout/stderr em storage/logs/launcher_console.log e
558
+ // ignora EPIPE (terminal fechado) — regra inviolável do AGENTS.md.
559
+ const homeLauncher = resolveHome(args);
560
+ const LOG_DIR = path.join(homeLauncher, 'storage', 'logs');
561
+ mkdirSync(LOG_DIR, { recursive: true });
562
+ const CONSOLE_LOG = path.join(LOG_DIR, 'launcher_console.log');
563
+ [process.stdout, process.stderr].forEach((stream) => guardConsoleStream(stream, CONSOLE_LOG));
564
+
501
565
  switch (command) {
502
566
  case 'install': cmdInstall(args); break;
503
567
  case 'start': cmdStart(args); break;