@danhachuel/leadmaster 0.1.14 → 0.1.16

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.
Files changed (27) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/backend/__pycache__/main.cpython-311.pyc +0 -0
  3. package/backend/main.py +24 -0
  4. package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
  6. package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
  7. package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
  8. package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
  9. package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
  10. package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
  11. package/backend/services/agents.py +4 -1
  12. package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
  13. package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  14. package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  15. package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  16. package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  17. package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
  18. package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  19. package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
  20. package/backend/tests/test_campaigns_routes.py +71 -0
  21. package/backend/tests/test_launcher_protections.py +28 -3
  22. package/frontend/dist/assets/{index-Bf28IekR.css → index-BKo_OphM.css} +1 -1
  23. package/frontend/dist/assets/index-BTNs0Fhp.js +90 -0
  24. package/frontend/dist/index.html +2 -2
  25. package/package.json +1 -1
  26. package/scripts/cli.mjs +139 -24
  27. package/frontend/dist/assets/index-DgsUzQ92.js +0 -90
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>LeadMaster — Painel</title>
8
- <script type="module" crossorigin src="/assets/index-DgsUzQ92.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Bf28IekR.css">
8
+ <script type="module" crossorigin src="/assets/index-BTNs0Fhp.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BKo_OphM.css">
10
10
  </head>
11
11
  <body class="bg-gray-100 min-h-screen">
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/leadmaster",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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
@@ -22,7 +22,7 @@
22
22
  * LEADMASTER_PORT Porta inicial da UI/API (mesmo efeito de --port).
23
23
  */
24
24
 
25
- import { spawn, spawnSync } from 'node:child_process';
25
+ import { spawn, spawnSync, execSync } from 'node:child_process';
26
26
  import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs';
27
27
  import { homedir, platform } from 'node:os';
28
28
  import net from 'node:net';
@@ -267,23 +267,20 @@ function portaLivre(port) {
267
267
 
268
268
  async function resolverPortaUI(portaInicial) {
269
269
  /**
270
- * Varre portas a partir de portaInicial (padrão 5055):
271
- * - se encontrar uma instância saudável do LeadMaster, devolve {jaRodando: true};
272
- * - senão devolve a primeira porta livre {jaRodando: false, porta};
273
- * - se nenhuma porta livre no intervalo, devolve {porta: null}.
270
+ * Devolve a primeira porta livre a partir de portaInicial (padrão 5055).
271
+ * Instâncias anteriores do LeadMaster já foram encerradas pelo kill switch
272
+ * (`encerrarInstanciaAnterior`) antes desta chamada; se uma porta seguir
273
+ * ocupada, é de outro projeto — o launcher avança (5056, 5057, ...).
274
+ * Se nenhuma porta livre no intervalo, devolve {porta: null}.
274
275
  */
275
276
  for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
276
277
  const porta = portaInicial + i;
277
- const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 800);
278
- if (saude.ok && saude.body && (saude.body.versao || saude.body.storage)) {
279
- return { jaRodando: true, porta };
280
- }
281
278
  if (await portaLivre(porta)) {
282
- return { jaRodando: false, porta };
279
+ return { porta };
283
280
  }
284
281
  console.log(`ℹ Porta ${porta} ocupada — tentando ${porta + 1}...`);
285
282
  }
286
- return { jaRodando: false, porta: null };
283
+ return { porta: null };
287
284
  }
288
285
 
289
286
  async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
@@ -297,6 +294,114 @@ async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
297
294
  return null;
298
295
  }
299
296
 
297
+ // ---------------------------------------------------------------------------
298
+ // Kill switch (regra inviolável do AGENTS.md)
299
+ //
300
+ // O launcher NUNCA deve ignorar uma porta ocupada por outra instância do
301
+ // LeadMaster: avisar que já existe instância no ar é PROIBIDO — isso impede
302
+ // atualizações (a versão antiga continua rodando, travando arquivos e
303
+ // causando EBUSY no npx). Antes de subir a nova versão, encerramos qualquer
304
+ // instância anterior confirmada.
305
+ //
306
+ // Segurança (higiene de processos do AGENTS.md): só mata processos
307
+ // CONFIRMADOS como LeadMaster — /api/health respondendo com o payload próprio
308
+ // (versao/home/storage) e o PID dono da porta. Processos de outros projetos
309
+ // NUNCA são tocados: nesse caso o launcher apenas avança para a próxima porta
310
+ // livre, como sempre.
311
+ // ---------------------------------------------------------------------------
312
+
313
+ function saudeEhLeadmaster(body) {
314
+ return Boolean(body && (body.versao || body.storage));
315
+ }
316
+
317
+ function pidsOuvindoPortaWindows(porta) {
318
+ try {
319
+ const out = execSync('netstat -ano', { windowsHide: true, encoding: 'utf8', timeout: 8000 });
320
+ const pids = new Set();
321
+ for (const linha of out.split(/\r?\n/)) {
322
+ if (!linha.includes('LISTENING')) continue;
323
+ const colunas = linha.trim().split(/\s+/);
324
+ if (colunas.length < 5) continue;
325
+ if (!colunas[1].endsWith(`:${porta}`)) continue;
326
+ const pid = Number(colunas[colunas.length - 1]);
327
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
328
+ }
329
+ return [...pids];
330
+ } catch {
331
+ return [];
332
+ }
333
+ }
334
+
335
+ function pidsOuvindoPortaUnix(porta) {
336
+ try {
337
+ const out = execSync(`lsof -ti:${porta} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] });
338
+ const pids = out.split(/\s+/).map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0);
339
+ return [...new Set(pids)];
340
+ } catch {
341
+ // Sem lsof: fallback amplo — processos do LeadMaster na lista de processos.
342
+ try {
343
+ const out = execSync('ps -eo pid=,args=', { encoding: 'utf8', timeout: 8000 });
344
+ const pids = new Set();
345
+ for (const linha of out.split('\n')) {
346
+ const m = linha.trim().match(/^(\d+)\s+(.*)$/);
347
+ if (!m) continue;
348
+ const pid = Number(m[1]);
349
+ const args = m[2];
350
+ const ehLeadmaster = args.includes('leadmaster') && (args.includes('cli.mjs') || args.includes('uvicorn main:app'));
351
+ if (pid > 0 && pid !== process.pid && ehLeadmaster) pids.add(pid);
352
+ }
353
+ return [...pids];
354
+ } catch {
355
+ return [];
356
+ }
357
+ }
358
+ }
359
+
360
+ function matarArvoreProcesso(pid) {
361
+ try {
362
+ if (platform() === 'win32') {
363
+ const r = spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
364
+ return r.status === 0;
365
+ }
366
+ process.kill(pid, 'SIGKILL');
367
+ return true;
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Kill switch: encerra toda instância anterior do LeadMaster (varre as portas
375
+ * candidatas) antes de subir a nova versão. Portas ocupadas por outros
376
+ * projetos são deixadas em paz (o resolvedor avançará para a próxima porta).
377
+ */
378
+ async function encerrarInstanciaAnterior(portaInicial) {
379
+ let encerrouAlguma = false;
380
+ for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
381
+ const porta = portaInicial + i;
382
+ const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
383
+ if (!saude.ok || !saudeEhLeadmaster(saude.body)) continue; // não é LeadMaster — respeitar
384
+ const pids = platform() === 'win32' ? pidsOuvindoPortaWindows(porta) : pidsOuvindoPortaUnix(porta);
385
+ if (pids.length === 0) continue;
386
+ console.log(`ℹ Instância anterior do LeadMaster detectada na porta ${porta} — encerrando (PID ${pids.join(', ')})...`);
387
+ for (const pid of pids) {
388
+ if (!matarArvoreProcesso(pid)) {
389
+ console.warn(`⚠ Não foi possível encerrar o PID ${pid} (permissão?). A nova instância usará outra porta.`);
390
+ }
391
+ }
392
+ encerrouAlguma = true;
393
+ // Aguarda o sistema operacional liberar a porta (até ~5s).
394
+ for (let t = 0; t < 10; t++) {
395
+ await sleep(500);
396
+ const ainda = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 600);
397
+ if (!ainda.ok) break;
398
+ }
399
+ }
400
+ if (encerrouAlguma) {
401
+ console.log('✓ Instância anterior encerrada — a versão nova assume o controle.');
402
+ }
403
+ }
404
+
300
405
  /**
301
406
  * Guarda da consola (anti-EPIPE): se o terminal for fechado (MobaXterm, VS
302
407
  * Code), escrever em stdout/stderr gera EPIPE — que o Node trataria como
@@ -331,10 +436,17 @@ function guardConsoleStream(stream, logFile) {
331
436
  // Comandos
332
437
  // ---------------------------------------------------------------------------
333
438
 
334
- function cmdInstall(args) {
439
+ async function cmdInstall(args) {
335
440
  const home = resolveHome(args);
441
+ const portaInicial = Number.isFinite(args.port) && args.port > 0 ? args.port : UI_PORT;
336
442
  console.log(`LeadMaster v${VERSION} — instalação local`);
337
443
  console.log('');
444
+
445
+ // Kill switch (regra inviolável do AGENTS.md): encerra instâncias anteriores
446
+ // ANTES de instalar — libera os arquivos travados no Windows (erro EBUSY do
447
+ // npx ao atualizar por cima de uma instância rodando).
448
+ await encerrarInstanciaAnterior(portaInicial);
449
+
338
450
  ensureHome(home);
339
451
  console.log(`✓ Pasta de dados pronta: ${home}`);
340
452
  console.log(` (leads, campanhas e configuração ficam em "${path.join(home, 'storage')}")`);
@@ -377,23 +489,19 @@ function cmdStart(args) {
377
489
  console.log(`✓ Pasta de dados: ${home}`);
378
490
 
379
491
  (async () => {
380
- // Resolve a porta: reusa instância ativa ou pega a primeira livre (5055 → 5056 → ...).
381
- const { jaRodando, porta } = await resolverPortaUI(portaInicial);
492
+ // Kill switch (regra inviolável do AGENTS.md): encerra qualquer instância
493
+ // anterior do LeadMaster (Node + Python) antes de subir a versão nova —
494
+ // nunca deixar a versão antiga no ar nem exibir aviso de reuso.
495
+ await encerrarInstanciaAnterior(portaInicial);
496
+
497
+ // Porta: primeira livre a partir de 5055 (→ 5056 → 5057 → ...).
498
+ const { porta } = await resolverPortaUI(portaInicial);
382
499
  if (porta === null) {
383
500
  console.error(`✗ Nenhuma porta livre entre ${portaInicial} e ${portaInicial + UI_PORT_MAX_TENTATIVAS - 1}.`);
384
501
  console.error(' Libere uma porta ou inicie com --port=<numero>.');
385
502
  process.exit(1);
386
503
  }
387
504
  const url = `http://127.0.0.1:${porta}`;
388
- if (jaRodando) {
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).
394
- process.exit(0);
395
- return;
396
- }
397
505
  if (porta !== portaInicial) {
398
506
  console.log(`ℹ Porta ${portaInicial} ocupada — usando a porta ${porta}.`);
399
507
  }
@@ -614,7 +722,14 @@ const CONSOLE_LOG = path.join(LOG_DIR, 'launcher_console.log');
614
722
  [process.stdout, process.stderr].forEach((stream) => guardConsoleStream(stream, CONSOLE_LOG));
615
723
 
616
724
  switch (command) {
617
- case 'install': cmdInstall(args); break;
725
+ case 'install':
726
+ // cmdInstall é async (kill switch): a promise mantém o evento vivo até o
727
+ // fim; rejeições são logadas pelos handlers globais e encerram com código 1.
728
+ cmdInstall(args).catch((err) => {
729
+ console.error('✗ Erro inesperado na instalação:', err && err.message ? err.message : err);
730
+ process.exit(1);
731
+ });
732
+ break;
618
733
  case 'start': cmdStart(args); break;
619
734
  case 'doctor': cmdDoctor(args); break;
620
735
  case 'version': console.log(VERSION); break;