@danhachuel/leadmaster 0.1.10 → 0.1.11
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 +31 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_launcher_protections.py +41 -3
- package/package.json +1 -1
- package/scripts/cli.mjs +47 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ 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.11] — 2026-11-08
|
|
10
|
+
|
|
11
|
+
### Corrigido
|
|
12
|
+
|
|
13
|
+
- **Launcher ainda devolvia o prompt no MobaXterm — a 0.1.9 não resolveu** — o
|
|
14
|
+
processo Node encerrava seu event loop logo após imprimir "LeadMaster no ar...",
|
|
15
|
+
mesmo com o keep-alive `setInterval`. Sem o Node vivo, o MobaXterm fecha a
|
|
16
|
+
pseudo-console (PTY) e o uvicorn perde o terminal, encerrando junto. Correções
|
|
17
|
+
definitivas no `scripts/cli.mjs`:
|
|
18
|
+
- **Terminal preso:** `process.stdin.resume();` inserido **logo antes das
|
|
19
|
+
mensagens finais de sucesso** — o Node fica indefinidamente vivo ouvindo a
|
|
20
|
+
entrada padrão, sem depender de timers nem do wrapper do `npx`. O keep-alive
|
|
21
|
+
`setInterval` permanece como segunda camada de proteção.
|
|
22
|
+
- **Ciclo de vida vinculado nos dois sentidos:** `child.on('close')` encerra o
|
|
23
|
+
launcher quando o servidor Python cai (mensagem "Servidor encerrado"), e
|
|
24
|
+
`process.on('exit')` mata o filho quando o launcher morre — nenhum processo
|
|
25
|
+
fica órfão. O worker Python continua **sem `.unref()`** (o pai não pode
|
|
26
|
+
morrer sozinho).
|
|
27
|
+
- **Erros globais (debug):** handlers `unhandledRejection` e `uncaughtException`
|
|
28
|
+
logam o erro em vez de derrubar o launcher silenciosamente.
|
|
29
|
+
- **Sem `process.exit(0)` após o sucesso:** a saída do fluxo principal agora é
|
|
30
|
+
natural (via listeners do filho); chamadas de `process.exit` permanecem apenas
|
|
31
|
+
em caminhos de erro acionável, na parada legítima (`SIGTERM`/`SIGBREAK`) e no
|
|
32
|
+
aviso informativo "já está rodando".
|
|
33
|
+
- **AGENTS.md:** regra inviolável do launcher passa de quatro para **cinco
|
|
34
|
+
proteções** (nova: *Terminal preso*, incluindo a proibição de `process.exit(0)`
|
|
35
|
+
após o sucesso e a vedação do `.unref()`).
|
|
36
|
+
- **Gate de regressão reforçado:** `backend/tests/test_launcher_protections.py`
|
|
37
|
+
falha se `process.stdin.resume();` desaparecer, se `.unref()` aparecer, se o
|
|
38
|
+
ciclo de vida vinculado for removido ou se os handlers globais sumirem.
|
|
39
|
+
|
|
9
40
|
## [0.1.10] — 2026-11-08
|
|
10
41
|
|
|
11
42
|
### Adicionado
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,12 +4,16 @@ Lê o launcher e falha se qualquer proteção contra encerramento silencioso no
|
|
|
4
4
|
Windows (MobaXterm) for removida:
|
|
5
5
|
|
|
6
6
|
1. Keep-alive com setInterval (event loop ativo sem I/O de console);
|
|
7
|
-
2.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
2. Terminal preso com `process.stdin.resume();` (solução definitiva contra a
|
|
8
|
+
devolução do prompt — o Node fica vivo ouvindo a entrada padrão);
|
|
9
|
+
3. Ausência de `.unref()` no worker Python (o pai não pode morrer sozinho);
|
|
10
|
+
4. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
|
|
11
|
+
5. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
|
|
12
|
+
6. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
from pathlib import Path
|
|
16
|
+
import re
|
|
13
17
|
|
|
14
18
|
CAMINHO = Path(__file__).resolve().parents[2] / "scripts" / "cli.mjs"
|
|
15
19
|
|
|
@@ -19,10 +23,38 @@ def _conteudo() -> str:
|
|
|
19
23
|
return CAMINHO.read_text(encoding="utf-8")
|
|
20
24
|
|
|
21
25
|
|
|
26
|
+
def _codigo_sem_comentarios() -> str:
|
|
27
|
+
"""Código efetivo do launcher (comentários removidos).
|
|
28
|
+
|
|
29
|
+
Regras de arquitetura valem para o CÓDIGO: menções em comentários
|
|
30
|
+
(explicações, nomes de funções proibidas) não contam.
|
|
31
|
+
"""
|
|
32
|
+
sem_bloco = re.sub(r"/\*.*?\*/", "", _conteudo(), flags=re.DOTALL)
|
|
33
|
+
sem_linha = re.sub(r"//[^\n]*", "", sem_bloco)
|
|
34
|
+
return sem_linha
|
|
35
|
+
|
|
36
|
+
|
|
22
37
|
class TestLauncherProtections:
|
|
23
38
|
def test_keepalivo(self):
|
|
24
39
|
assert "setInterval(() => {}, 1000)" in _conteudo()
|
|
25
40
|
|
|
41
|
+
def test_stdin_resume_prende_terminal(self):
|
|
42
|
+
# Solução definitiva: sem esta linha o MobaXterm devolve o prompt
|
|
43
|
+
# logo após "LeadMaster no ar..." e fecha a PTY (e o uvicorn junto).
|
|
44
|
+
assert "process.stdin.resume();" in _conteudo()
|
|
45
|
+
|
|
46
|
+
def test_worker_nunca_unref(self):
|
|
47
|
+
# O filho Python referenciado mantém o pai vivo; .unref() permitiria
|
|
48
|
+
# a morte prematura do launcher. Comentários podem mencionar a regra.
|
|
49
|
+
assert ".unref()" not in _codigo_sem_comentarios()
|
|
50
|
+
|
|
51
|
+
def test_ciclo_de_vida_vinculado(self):
|
|
52
|
+
# Python cai → launcher cai (child 'close'); launcher morre → filho morre.
|
|
53
|
+
c = _conteudo()
|
|
54
|
+
assert "child.on('close'" in c
|
|
55
|
+
assert "process.on('exit'" in c
|
|
56
|
+
assert "Servidor encerrado" in c
|
|
57
|
+
|
|
26
58
|
def test_absorcao_de_sinais_no_windows(self):
|
|
27
59
|
c = _conteudo()
|
|
28
60
|
assert "SIGINT" in c and "SIGTERM" in c and "SIGHUP" in c
|
|
@@ -44,3 +76,9 @@ class TestLauncherProtections:
|
|
|
44
76
|
c = _conteudo()
|
|
45
77
|
assert "SIGTERM" in c and "SIGBREAK" in c
|
|
46
78
|
assert "naoAbsorver" in c
|
|
79
|
+
|
|
80
|
+
def test_erros_globais_logados(self):
|
|
81
|
+
# Erros silenciosos (rejeições/exceções) são logados, não matam o launcher.
|
|
82
|
+
c = _conteudo()
|
|
83
|
+
assert "unhandledRejection" in c
|
|
84
|
+
assert "uncaughtException" in c
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -30,6 +30,21 @@ import path from 'node:path';
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import http from 'node:http';
|
|
32
32
|
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Erros globais (blindagem anti-encerramento silencioso)
|
|
35
|
+
// Rejeições e exceções não tratadas são APENAS LOGADAS — nunca devem derrubar
|
|
36
|
+
// o launcher sem explicação (era assim que o MobaXterm perdia o processo sem
|
|
37
|
+
// deixar rastro). O launcher só termina pelo fluxo normal: erro acionável,
|
|
38
|
+
// parada legítima (SIGTERM/SIGBREAK) ou morte do servidor Python filho.
|
|
39
|
+
// NUNCA REMOVER (regra inviolável do AGENTS.md).
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
process.on('unhandledRejection', (reason) => {
|
|
42
|
+
console.error('Unhandled Rejection:', reason);
|
|
43
|
+
});
|
|
44
|
+
process.on('uncaughtException', (err) => {
|
|
45
|
+
console.error('Uncaught Exception:', err);
|
|
46
|
+
});
|
|
47
|
+
|
|
33
48
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
49
|
|
|
35
50
|
// Porta padrão da UI/API local; se estiver ocupada, o launcher avança
|
|
@@ -372,6 +387,10 @@ function cmdStart(args) {
|
|
|
372
387
|
const url = `http://127.0.0.1:${porta}`;
|
|
373
388
|
if (jaRodando) {
|
|
374
389
|
console.log(`ℹ LeadMaster já está rodando em ${url} — abra o endereço no navegador.`);
|
|
390
|
+
// Saída INFORMATIVA (antes de qualquer inicialização própria): não há
|
|
391
|
+
// servidor filho para supervisionar nem terminal para prender — o
|
|
392
|
+
// launcher não deve chamar process.exit(0) no caminho de SUCESSO da
|
|
393
|
+
// inicialização (regra inviolável do AGENTS.md).
|
|
375
394
|
process.exit(0);
|
|
376
395
|
return;
|
|
377
396
|
}
|
|
@@ -438,6 +457,14 @@ function cmdStart(args) {
|
|
|
438
457
|
process.exit(code === null ? 1 : code);
|
|
439
458
|
});
|
|
440
459
|
|
|
460
|
+
// Ciclo de vida vinculado (regra inviolável do AGENTS.md): se o Python
|
|
461
|
+
// cai, o launcher cai junto — nunca fica preso ao terminal sem servidor.
|
|
462
|
+
// O worker NUNCA é criado com .unref(): o pai não pode morrer sozinho.
|
|
463
|
+
child.on('close', (code) => {
|
|
464
|
+
console.log('Servidor encerrado');
|
|
465
|
+
process.exit(code === null ? 1 : code);
|
|
466
|
+
});
|
|
467
|
+
|
|
441
468
|
const stop = () => {
|
|
442
469
|
console.log('\nEncerrando LeadMaster...');
|
|
443
470
|
if (platform() === 'win32' && child.pid) {
|
|
@@ -447,6 +474,16 @@ function cmdStart(args) {
|
|
|
447
474
|
}
|
|
448
475
|
};
|
|
449
476
|
|
|
477
|
+
// Morte do launcher → morte do filho (e vice-versa, via close acima):
|
|
478
|
+
// o uvicorn nunca fica órfão no terminal quando o Node é encerrado.
|
|
479
|
+
process.on('exit', () => {
|
|
480
|
+
try {
|
|
481
|
+
child.kill('SIGTERM');
|
|
482
|
+
} catch {
|
|
483
|
+
/* filho já encerrou — nada a fazer */
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
450
487
|
console.log(`→ Subindo servidor local em ${url} ...`);
|
|
451
488
|
const health = await waitForHealth(porta, 120000);
|
|
452
489
|
if (!health) {
|
|
@@ -476,10 +513,18 @@ function cmdStart(args) {
|
|
|
476
513
|
process.on('SIGBREAK', naoAbsorver);
|
|
477
514
|
}
|
|
478
515
|
|
|
479
|
-
// Keep-alive: mantém o event loop ativo
|
|
480
|
-
// minimizado / sem stream ativo)
|
|
516
|
+
// Keep-alive (regra inviolável do AGENTS.md): mantém o event loop ativo
|
|
517
|
+
// mesmo sem I/O de console (MobaXterm minimizado / sem stream ativo).
|
|
481
518
|
setInterval(() => {}, 1000);
|
|
482
519
|
|
|
520
|
+
// Terminal preso (regra inviolável do AGENTS.md) — SOLUÇÃO DEFINITIVA:
|
|
521
|
+
// ao retomar a entrada padrão IMEDIATAMENTE antes das mensagens finais
|
|
522
|
+
// de sucesso, o Node permanece vivo indefinidamente aguardando qualquer
|
|
523
|
+
// digitação do usuário. O prompt do MobaXterm NUNCA é devolvido e a PTY
|
|
524
|
+
// (e com ela o uvicorn) não é fechada. Esta linha NÃO depende de timers
|
|
525
|
+
// nem de npx — NUNCA REMOVER.
|
|
526
|
+
process.stdin.resume();
|
|
527
|
+
|
|
483
528
|
console.log(`✓ LeadMaster no ar em ${url} (dados: ${home})`);
|
|
484
529
|
console.log('');
|
|
485
530
|
console.log(`ℹ Abra ${url} no navegador quando quiser — o LeadMaster nunca abre janelas sozinho.`);
|