@danhachuel/leadmaster 0.1.14 → 0.1.15

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
@@ -37,6 +37,36 @@ normaliza para a forma canônica (ex.: `0.1.0`).
37
37
  falha se `process.stdin.resume();` desaparecer, se `.unref()` aparecer, se o
38
38
  ciclo de vida vinculado for removido ou se os handlers globais sumirem.
39
39
 
40
+ ## [0.1.15] — 2026-11-08
41
+
42
+ ### Corrigido
43
+
44
+ - **Launcher — kill switch: a versão nova SEMPRE assume o controle** — o
45
+ bloqueio de porta foi eliminado. Antes, ao detectar a porta ocupada, o
46
+ `cli.mjs` apenas imprimia "LeadMaster já está rodando" e encerrava: a versão
47
+ antiga continuava no ar, o usuário não conseguia testar a nova e o `npx`
48
+ falhava com `EBUSY` (arquivos travados no Windows). Agora:
49
+ - **`encerrarInstanciaAnterior()`** roda no início do `install` (libera os
50
+ arquivos antes de instalar — fim do `EBUSY`) e do `start`;
51
+ - descoberta do PID dono da porta via `netstat -ano` (Windows) /
52
+ `lsof -ti:porta` (Linux/macOS, com fallback `ps`), seguida de
53
+ `taskkill /F /T /PID` ou `kill -9`;
54
+ - espera de até 5s pela liberação da porta antes de subir o novo servidor;
55
+ - **higiene preservada:** só mata processos **confirmados** como LeadMaster
56
+ (`/api/health` com payload próprio + PID dono da porta). Portas ocupadas
57
+ por outros projetos são respeitadas — o launcher avança para a próxima
58
+ porta livre, como sempre;
59
+ - ciclo de vida vinculado mantido: com o Python antigo morto, o Node antigo
60
+ encerra sozinho (`child.on('exit')`/`close`).
61
+ - **AGENTS.md:** 6ª regra inviolável do launcher — *Kill switch*: nunca ignorar
62
+ porta ocupada por instância do LeadMaster; mensagem de reuso **proibida**.
63
+ - **Gate de regressão:** `test_launcher_protections.py` falha se o kill switch
64
+ desaparecer (`encerrarInstanciaAnterior`/`taskkill`/`netstat`/`lsof`) ou se a
65
+ mensagem proibida voltar. Suíte completa: **32 passed**.
66
+ - **Testado ao vivo:** instância anterior na mesma porta detectada, encerrada
67
+ (PID confirmado) e nova instância assumida com PID novo; node antigo caiu
68
+ pelo ciclo de vida; instância de outros projetos preservada.
69
+
40
70
  ## [0.1.14] — 2026-11-08
41
71
 
42
72
  ### Adicionado
@@ -6,9 +6,11 @@ Windows (MobaXterm) for removida:
6
6
  1. Keep-alive com setInterval (event loop ativo sem I/O de console);
7
7
  2. Terminal preso: `process.stdin.resume();` (PTY do MobaXterm nunca fecha);
8
8
  sem `process.exit(0)` no caminho de sucesso e sem `.unref()` no worker;
9
- 3. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
10
- 4. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
11
- 5. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
9
+ 3. Kill switch: instância anterior do LeadMaster encerrada antes de subir a
10
+ nova versão (taskkill/lsof pelo PID da porta) — nunca reuso de instância;
11
+ 4. Absorção de SIGINT/SIGHUP no Windows (apenas log, sem encerrar);
12
+ 5. Função guardConsoleStream (anti-EPIPE com espelho em arquivo de log);
13
+ 6. Isolamento do worker Python (detached: true / CREATE_NEW_PROCESS_GROUP).
12
14
  """
13
15
 
14
16
  from pathlib import Path
@@ -31,6 +33,17 @@ class TestLauncherProtections:
31
33
  def test_worker_nunca_unref(self):
32
34
  assert ".unref()" not in _conteudo()
33
35
 
36
+ def test_kill_switch_mata_instancia_anterior(self):
37
+ # Regra inviolável: nunca ignorar uma porta ocupada por outra instância
38
+ # do LeadMaster — matar o processo antigo antes de subir o novo. Sem
39
+ # isso a versão antiga trava arquivos (EBUSY no npx) e a nova não sobe.
40
+ c = _conteudo()
41
+ assert "encerrarInstanciaAnterior" in c
42
+ assert "taskkill" in c # Windows: mata a árvore do PID da porta
43
+ assert "netstat" in c or "lsof" in c # descoberta do PID pela porta
44
+ # Avisar que já existe instância no ar é proibido (impede atualizações).
45
+ assert "já está rodando" not in c
46
+
34
47
  def test_ciclo_de_vida_vinculado(self):
35
48
  c = _conteudo()
36
49
  assert "child.on('exit'" in c or "child.on(\"exit\"" in c
@@ -64,7 +64,7 @@ Error generating stack: `+i.message+`
64
64
  * LICENSE.md file in the root directory of this source tree.
65
65
  *
66
66
  * @license MIT
67
- */function Al(){return Al=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var s in n)({}).hasOwnProperty.call(n,s)&&(t[s]=n[s])}return t},Al.apply(null,arguments)}function py(t,e){if(t==null)return{};var n={};for(var s in t)if({}.hasOwnProperty.call(t,s)){if(e.indexOf(s)!==-1)continue;n[s]=t[s]}return n}function WS(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function HS(t,e){return t.button===0&&(!e||e==="_self")&&!WS(t)}function Rd(t){return t===void 0&&(t=""),new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let s=t[n];return e.concat(Array.isArray(s)?s.map(r=>[n,r]):[[n,s]])},[]))}function VS(t,e){let n=Rd(t);return e&&e.forEach((s,r)=>{n.has(r)||e.getAll(r).forEach(i=>{n.append(r,i)})}),n}const QS=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],KS=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],YS="6";try{window.__reactRouterVersion=YS}catch{}const XS=w.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=w.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=w.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=w.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return w.useLayoutEffect(()=>o.listen(d),[o,d]),w.useEffect(()=>BS(s),[s]),w.createElement($S,{basename:e,children:n,location:a.location,navigationType:a.action,navigator:o,future:s})}const JS=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ZS=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,e_=w.forwardRef(function(e,n){let{onClick:s,relative:r,reloadDocument:i,replace:o,state:a,target:l,to:c,preventScrollReset:d,viewTransition:f}=e,h=py(e,QS),{basename:p}=w.useContext(Os),x,g=!1;if(typeof c=="string"&&ZS.test(c)&&(x=c,JS))try{let v=new URL(window.location.href),S=c.startsWith("//")?new URL(v.protocol+c):new URL(c),_=pi(S.pathname,p);S.origin===v.origin&&_!=null?c=_+S.search+S.hash:g=!0}catch{}let b=CS(c,{relative:r}),y=n_(c,{replace:o,state:a,target:l,preventScrollReset:d,relative:r,viewTransition:f});function m(v){s&&s(v),v.defaultPrevented||y(v)}return w.createElement("a",Al({},h,{href:x||b,onClick:g||i?s:m,ref:n,target:l}))}),gy=w.forwardRef(function(e,n){let{"aria-current":s="page",caseSensitive:r=!1,className:i="",end:o=!1,style:a,to:l,viewTransition:c,children:d}=e,f=py(e,KS),h=vc(l,{relative:f.relative}),p=Ts(),x=w.useContext(ly),{navigator:g,basename:b}=w.useContext(Os),y=x!=null&&r_(h)&&c===!0,m=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,S=x&&x.navigation&&x.navigation.location?x.navigation.location.pathname:null;r||(v=v.toLowerCase(),S=S?S.toLowerCase():null,m=m.toLowerCase()),S&&b&&(S=pi(S,b)||S);const _=m!=="/"&&m.endsWith("/")?m.length-1:m.length;let k=v===m||!o&&v.startsWith(m)&&v.charAt(_)==="/",C=S!=null&&(S===m||!o&&S.startsWith(m)&&S.charAt(m.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},P=k?s:void 0,E;typeof i=="function"?E=i(j):E=[i,k?"active":null,C?"pending":null,y?"transitioning":null].filter(Boolean).join(" ");let R=typeof a=="function"?a(j):a;return w.createElement(e_,Al({},f,{"aria-current":P,className:E,ref:n,style:R,to:l,viewTransition:c}),typeof d=="function"?d(j):d)});var Od;(function(t){t.UseScrollRestoration="useScrollRestoration",t.UseSubmit="useSubmit",t.UseSubmitFetcher="useSubmitFetcher",t.UseFetcher="useFetcher",t.useViewTransitionState="useViewTransitionState"})(Od||(Od={}));var Kp;(function(t){t.UseFetcher="useFetcher",t.UseFetchers="useFetchers",t.UseScrollRestoration="useScrollRestoration"})(Kp||(Kp={}));function t_(t){let e=w.useContext(xc);return e||je(!1),e}function n_(t,e){let{target:n,replace:s,state:r,preventScrollReset:i,relative:o,viewTransition:a}=e===void 0?{}:e,l=bi(),c=Ts(),d=vc(t,{relative:o});return w.useCallback(f=>{if(HS(f,n)){f.preventDefault();let h=s!==void 0?s:Ll(c)===Ll(d);l(t,{replace:h,state:r,preventScrollReset:i,relative:o,viewTransition:a})}},[c,l,d,s,r,n,t,i,o,a])}function s_(t){let e=w.useRef(Rd(t)),n=w.useRef(!1),s=Ts(),r=w.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=w.useCallback((a,l)=>{const c=Rd(typeof a=="function"?a(r):a);n.current=!0,i("?"+c,l)},[i,r]);return[r,o]}function r_(t,e){e===void 0&&(e={});let n=w.useContext(XS);n==null&&je(!1);let{basename:s}=t_(Od.useViewTransitionState),r=vc(t,{relative:e.relative});if(!n.isTransitioning)return!1;let i=pi(n.currentLocation.pathname,s)||n.currentLocation.pathname,o=pi(n.nextLocation.pathname,s)||n.nextLocation.pathname;return Md(r.pathname,o)!=null||Md(r.pathname,i)!=null}const i_="/api";class o_ extends Error{constructor(n,s){super(s);$(this,"status");this.status=n,this.name="ApiError"}}async function Y(t,e={}){const n=await fetch(i_+t,{...e,headers:e.body?{"Content-Type":"application/json",...e.headers||{}}:e.headers});if(!n.ok){let s=`Erro HTTP ${n.status}`;try{const r=await n.json();s=String(r.detail||r.mensagem||s)}catch{}throw new o_(n.status,s)}if(n.status!==204)return n.json()}const Me=t=>({method:"POST",body:JSON.stringify(t)}),W={health:()=>Y("/health"),doctor:()=>Y("/doctor"),shutdown:()=>Y("/shutdown",Me({})),metricas:()=>Y("/metrics"),leads:{listar:()=>Y("/leads"),criar:t=>Y("/leads",Me(t)),atualizar:(t,e)=>Y(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>Y(`/leads/${t}`,{method:"DELETE"}),importar:t=>Y("/leads/import",Me({leads:t})),enriquecer:t=>Y(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>Y("/crm/leads"),criarLead:t=>Y("/crm/leads",Me(t)),moverLead:(t,e)=>Y(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>Y("/crm/sugestoes"),aplicar:(t,e)=>Y("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>Y("/campaigns"),criar:t=>Y("/campaigns",Me(t)),iniciar:t=>Y(`/campaigns/${t}/start`,Me({})),pausar:t=>Y(`/campaigns/${t}/pause`,Me({})),parar:t=>Y(`/campaigns/${t}/stop`,Me({})),remover:t=>Y(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>Y("/extractors/maps",Me(t)),instagram:(t,e)=>Y("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>Y("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>Y("/extract/cnpj/search",Me(t)),cnpjExtract:t=>Y("/extractors/cnpj",Me(t))},config:{obter:()=>Y("/config"),salvar:t=>Y("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>Y("/config/test/llm",Me({})),testarIntegracao:t=>Y("/config/test/integracao",Me({url:t}))},integracoes:()=>Y("/integrations/status"),agentes:{templates:()=>Y("/agents/templates"),salvarTemplates:t=>Y("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>Y("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>Y("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>Y("/webhook"),salvar:t=>Y("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>Y("/webhook/test",Me({url:t}))}};function Q(...t){return t.filter(Boolean).join(" ")}function a_(t){return(t||"?").trim().charAt(0).toUpperCase()||"?"}function Il(t){if(!t)return"—";try{return new Date(t).toLocaleString("pt-BR",{day:"2-digit",month:"2-digit",year:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return t}}function na(t){const e=(t||"").replace(/\D/g,"");return e?e.length===11?`(${e.slice(0,2)}) ${e.slice(2,7)}-${e.slice(7)}`:e.length===10?`(${e.slice(0,2)}) ${e.slice(2,6)}-${e.slice(6)}`:t||"—":"—"}function l_(t){const e=["nome","telefone","email","empresa","cidade","observacoes"],n=e.filter(s=>String(t[s]||"").trim().length>0);return Math.round(n.length/e.length*100)}function c_(t){const e={nome:"nome",telefone:"telefone",email:"e-mail",empresa:"empresa",cidade:"cidade",observacoes:"observações"};return["nome","telefone","email","empresa","cidade","observacoes"].filter(s=>String(t[s]||"").trim().length===0).map(s=>e[s])}function u_(t,e){if(t>=100)return"Lead 100% completo — excelente! 🎉";const n=e.slice(0,2).join(" e ");return`Faltam ${100-t}% (${n}${e.length>2?"...":""}) para o lead ficar completo`}function d_(t,e){const n=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),s=URL.createObjectURL(n),r=document.createElement("a");r.href=s,r.download=e,r.click(),URL.revokeObjectURL(s)}const f_={md:"max-w-lg",lg:"max-w-2xl",xl:"max-w-4xl"};function Xt({aberto:t,onFechar:e,titulo:n,children:s,icone:r="fas fa-circle-info",largura:i="md"}){return w.useEffect(()=>{const o=a=>{a.key==="Escape"&&e()};return t&&window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[t,e]),t?u.jsx("div",{className:"fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4",onMouseDown:o=>{o.target===o.currentTarget&&e()},children:u.jsxs("div",{className:Q("bg-white rounded-2xl shadow-xl w-full",f_[i]),children:[u.jsxs("div",{className:"flex items-center justify-between px-6 py-4 border-b border-gray-100",children:[u.jsxs("h3",{className:"text-base font-semibold text-gray-800 flex items-center gap-2",children:[u.jsx("i",{className:Q(r,"text-indigo-500")})," ",n]}),u.jsx("button",{onClick:e,className:"text-gray-400 hover:text-gray-600","aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]}),u.jsx("div",{className:"px-6 py-5 max-h-[70vh] overflow-y-auto",children:s})]})}):null}const h_=[{icone:"fas fa-users",texto:"Cadastre leads manualmente ou importe pelos Extratores (Maps, Instagram, CNPJ)."},{icone:"fas fa-columns",texto:"Arraste os cards no CRM conforme avança no funil (Leads → Em contato → Proposta → Fechado)."},{icone:"fas fa-bullhorn",texto:"Monte campanhas de disparo com seus templates e acompanhe as taxas no Painel."},{icone:"fas fa-robot",texto:"Use os Agentes (disparo e atendimento) e configure o LLM em Ferramentas para turbiná-los."}];function p_({titulo:t,nomeUsuario:e}){const[n,s]=w.useState(!1);return u.jsxs("header",{className:"bg-white shadow-sm px-8 py-4 flex items-center justify-between shrink-0",children:[u.jsx("div",{children:u.jsx("h2",{className:"text-lg font-semibold text-gray-800",children:t})}),u.jsxs("div",{className:"flex items-center space-x-4",children:[u.jsxs("button",{onClick:()=>s(!0),className:"text-sm text-indigo-600 hover:text-indigo-800 font-medium",children:[u.jsx("i",{className:"fas fa-play-circle mr-1"})," Ver tutorial"]}),u.jsx("div",{className:"w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-bold text-sm",children:a_(e)}),u.jsx("span",{className:"text-sm font-medium text-gray-700",children:e})]}),u.jsxs(Xt,{aberto:n,onFechar:()=>s(!1),titulo:"Tutorial rápido",icone:"fas fa-graduation-cap",children:[u.jsx("ol",{className:"space-y-4",children:h_.map((r,i)=>u.jsxs("li",{className:"flex gap-3 items-start",children:[u.jsx("span",{className:"w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0",children:u.jsx("i",{className:r.icone})}),u.jsx("p",{className:"text-sm text-gray-600 pt-1.5",children:r.texto})]},i))}),u.jsx("p",{className:"text-xs text-gray-400 mt-5",children:"Dica: o LLM (para enriquecimento e agentes) é configurado em Ferramentas."})]})]})}const g_=[{itens:[{para:"/",icone:"fas fa-chart-pie text-indigo-300",rotulo:"Painel",fim:!0}]},{secao:"Conexões",itens:[{para:"/leads",icone:"fas fa-users",rotulo:"Leads"},{para:"/agentes",icone:"fas fa-robot",rotulo:"Agentes"},{para:"/ferramentas",icone:"fas fa-tools",rotulo:"Ferramentas"}]},{itens:[{para:"/extrator-maps",icone:"fas fa-map-marked-alt",rotulo:"Extrator Maps"},{para:"/extrator-instagram",icone:"fab fa-instagram",rotulo:"Extrator Instagram"},{para:"/extrator-cnpj",icone:"fas fa-building",rotulo:"Extrator CNPJ"}]},{secao:"Gestão",itens:[{para:"/disparos",icone:"fas fa-bullhorn",rotulo:"Disparos"},{para:"/crm",icone:"fas fa-columns",rotulo:"CRM"}]},{secao:"Integrações",itens:[{para:"/webhook",icone:"fas fa-link",rotulo:"Webhook"},{para:"/suporte",icone:"fas fa-headset",rotulo:"Suporte"}]}];function m_({onSair:t}){const e=Ts(),[n,s]=w.useState(!1);w.useEffect(()=>{e.pathname==="/agentes"&&s(!0)},[e.pathname]);const r=i=>{if(i.rotulo==="Agentes")return e.pathname==="/agentes";const[o,a]=i.para.split("?");return a?e.pathname===o&&e.search.includes(a):i.fim?e.pathname===o:e.pathname.startsWith(o)};return u.jsxs("aside",{className:"w-64 bg-[#1e1b4b] text-white flex flex-col shrink-0 h-screen sticky top-0",children:[u.jsx("div",{className:"p-6 border-b border-gray-700",children:u.jsxs("h1",{className:"text-2xl font-bold tracking-tight flex items-center gap-2",children:[u.jsx("span",{className:"text-2xl",children:"⚡"}),"LeadMaster",u.jsxs("span",{className:"text-[10px] font-medium text-indigo-300 border border-indigo-400/40 rounded-md px-1.5 py-0.5",children:["v","0.1.14"]})]})}),u.jsx("nav",{className:"flex-1 overflow-y-auto py-4",children:u.jsx("ul",{className:"space-y-1",children:g_.map((i,o)=>u.jsx("li",{children:u.jsxs("ul",{children:[i.secao&&u.jsx("li",{className:"px-6 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider list-none",children:i.secao}),i.itens.map(a=>a.rotulo==="Agentes"?u.jsxs("li",{children:[u.jsxs("button",{type:"button",onClick:()=>s(l=>!l),className:Q("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:Q("fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform",n&&"rotate-180")})]}),n&&u.jsxs("ul",{className:"pl-12 space-y-1 mt-1",children:[u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=disparo",rotulo:"Agente de Disparo",ativo:e.pathname==="/agentes"&&e.search.includes("disparo")})}),u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=atendimento",rotulo:"Agente de Atendimento",ativo:e.pathname==="/agentes"&&e.search.includes("atendimento")})})]})]},"agentes"):u.jsx("li",{children:u.jsxs(gy,{to:a.para,className:Q("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo})]})},a.para))]})},o))})}),u.jsx("div",{className:"p-4 border-t border-gray-700",children:u.jsxs("button",{onClick:t,className:"w-full flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg text-sm font-medium hover:opacity-90 transition",children:[u.jsx("i",{className:"fas fa-sign-out-alt mr-2"})," Sair"]})})]})}function Yp({para:t,rotulo:e,ativo:n}){return u.jsx(gy,{to:t,className:Q("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=w.createContext({toast:()=>{}});function en(){return w.useContext(my)}const x_={sucesso:"bg-green-600",erro:"bg-red-600",info:"bg-indigo-600"},y_={sucesso:"fas fa-check-circle",erro:"fas fa-exclamation-circle",info:"fas fa-info-circle"};function v_({children:t}){const[e,n]=w.useState([]),s=w.useRef(0),r=w.useCallback((i,o)=>{const a=++s.current;n(l=>[...l,{id:a,tipo:i,mensagem:o}]),setTimeout(()=>n(l=>l.filter(c=>c.id!==a)),4500)},[]);return u.jsxs(my.Provider,{value:{toast:r},children:[t,u.jsx("div",{className:"fixed top-4 right-4 z-[100] space-y-2 w-80",children:e.map(i=>u.jsxs("div",{className:Q(x_[i.tipo],"text-white text-sm rounded-lg shadow-lg px-4 py-3 flex items-start gap-2 animate-pulse-once"),role:"status",children:[u.jsx("i",{className:Q(y_[i.tipo],"mt-0.5")}),u.jsx("span",{className:"flex-1",children:i.mensagem}),u.jsx("button",{className:"opacity-70 hover:opacity-100",onClick:()=>n(o=>o.filter(a=>a.id!==i.id)),"aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]},i.id))})]})}function Pa({rotulo:t,valor:e,subtitulo:n,children:s,icone:r}){return u.jsxs("div",{className:"bg-white rounded-xl shadow-sm p-5 border border-gray-100",children:[u.jsxs("div",{className:"flex items-center justify-between",children:[u.jsx("p",{className:"text-sm text-gray-500 font-medium",children:t}),r&&u.jsx("i",{className:`${r} text-indigo-300`})]}),s??u.jsxs(u.Fragment,{children:[u.jsx("p",{className:"text-2xl font-bold text-gray-800 mt-1",children:e}),n&&u.jsx("p",{className:"text-xs text-gray-400 mt-0.5",children:n})]})]})}/*!
67
+ */function Al(){return Al=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var s in n)({}).hasOwnProperty.call(n,s)&&(t[s]=n[s])}return t},Al.apply(null,arguments)}function py(t,e){if(t==null)return{};var n={};for(var s in t)if({}.hasOwnProperty.call(t,s)){if(e.indexOf(s)!==-1)continue;n[s]=t[s]}return n}function WS(t){return!!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)}function HS(t,e){return t.button===0&&(!e||e==="_self")&&!WS(t)}function Rd(t){return t===void 0&&(t=""),new URLSearchParams(typeof t=="string"||Array.isArray(t)||t instanceof URLSearchParams?t:Object.keys(t).reduce((e,n)=>{let s=t[n];return e.concat(Array.isArray(s)?s.map(r=>[n,r]):[[n,s]])},[]))}function VS(t,e){let n=Rd(t);return e&&e.forEach((s,r)=>{n.has(r)||e.getAll(r).forEach(i=>{n.append(r,i)})}),n}const QS=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],KS=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],YS="6";try{window.__reactRouterVersion=YS}catch{}const XS=w.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=w.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=w.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=w.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return w.useLayoutEffect(()=>o.listen(d),[o,d]),w.useEffect(()=>BS(s),[s]),w.createElement($S,{basename:e,children:n,location:a.location,navigationType:a.action,navigator:o,future:s})}const JS=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ZS=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,e_=w.forwardRef(function(e,n){let{onClick:s,relative:r,reloadDocument:i,replace:o,state:a,target:l,to:c,preventScrollReset:d,viewTransition:f}=e,h=py(e,QS),{basename:p}=w.useContext(Os),x,g=!1;if(typeof c=="string"&&ZS.test(c)&&(x=c,JS))try{let v=new URL(window.location.href),S=c.startsWith("//")?new URL(v.protocol+c):new URL(c),_=pi(S.pathname,p);S.origin===v.origin&&_!=null?c=_+S.search+S.hash:g=!0}catch{}let b=CS(c,{relative:r}),y=n_(c,{replace:o,state:a,target:l,preventScrollReset:d,relative:r,viewTransition:f});function m(v){s&&s(v),v.defaultPrevented||y(v)}return w.createElement("a",Al({},h,{href:x||b,onClick:g||i?s:m,ref:n,target:l}))}),gy=w.forwardRef(function(e,n){let{"aria-current":s="page",caseSensitive:r=!1,className:i="",end:o=!1,style:a,to:l,viewTransition:c,children:d}=e,f=py(e,KS),h=vc(l,{relative:f.relative}),p=Ts(),x=w.useContext(ly),{navigator:g,basename:b}=w.useContext(Os),y=x!=null&&r_(h)&&c===!0,m=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,S=x&&x.navigation&&x.navigation.location?x.navigation.location.pathname:null;r||(v=v.toLowerCase(),S=S?S.toLowerCase():null,m=m.toLowerCase()),S&&b&&(S=pi(S,b)||S);const _=m!=="/"&&m.endsWith("/")?m.length-1:m.length;let k=v===m||!o&&v.startsWith(m)&&v.charAt(_)==="/",C=S!=null&&(S===m||!o&&S.startsWith(m)&&S.charAt(m.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},P=k?s:void 0,E;typeof i=="function"?E=i(j):E=[i,k?"active":null,C?"pending":null,y?"transitioning":null].filter(Boolean).join(" ");let R=typeof a=="function"?a(j):a;return w.createElement(e_,Al({},f,{"aria-current":P,className:E,ref:n,style:R,to:l,viewTransition:c}),typeof d=="function"?d(j):d)});var Od;(function(t){t.UseScrollRestoration="useScrollRestoration",t.UseSubmit="useSubmit",t.UseSubmitFetcher="useSubmitFetcher",t.UseFetcher="useFetcher",t.useViewTransitionState="useViewTransitionState"})(Od||(Od={}));var Kp;(function(t){t.UseFetcher="useFetcher",t.UseFetchers="useFetchers",t.UseScrollRestoration="useScrollRestoration"})(Kp||(Kp={}));function t_(t){let e=w.useContext(xc);return e||je(!1),e}function n_(t,e){let{target:n,replace:s,state:r,preventScrollReset:i,relative:o,viewTransition:a}=e===void 0?{}:e,l=bi(),c=Ts(),d=vc(t,{relative:o});return w.useCallback(f=>{if(HS(f,n)){f.preventDefault();let h=s!==void 0?s:Ll(c)===Ll(d);l(t,{replace:h,state:r,preventScrollReset:i,relative:o,viewTransition:a})}},[c,l,d,s,r,n,t,i,o,a])}function s_(t){let e=w.useRef(Rd(t)),n=w.useRef(!1),s=Ts(),r=w.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=w.useCallback((a,l)=>{const c=Rd(typeof a=="function"?a(r):a);n.current=!0,i("?"+c,l)},[i,r]);return[r,o]}function r_(t,e){e===void 0&&(e={});let n=w.useContext(XS);n==null&&je(!1);let{basename:s}=t_(Od.useViewTransitionState),r=vc(t,{relative:e.relative});if(!n.isTransitioning)return!1;let i=pi(n.currentLocation.pathname,s)||n.currentLocation.pathname,o=pi(n.nextLocation.pathname,s)||n.nextLocation.pathname;return Md(r.pathname,o)!=null||Md(r.pathname,i)!=null}const i_="/api";class o_ extends Error{constructor(n,s){super(s);$(this,"status");this.status=n,this.name="ApiError"}}async function Y(t,e={}){const n=await fetch(i_+t,{...e,headers:e.body?{"Content-Type":"application/json",...e.headers||{}}:e.headers});if(!n.ok){let s=`Erro HTTP ${n.status}`;try{const r=await n.json();s=String(r.detail||r.mensagem||s)}catch{}throw new o_(n.status,s)}if(n.status!==204)return n.json()}const Me=t=>({method:"POST",body:JSON.stringify(t)}),W={health:()=>Y("/health"),doctor:()=>Y("/doctor"),shutdown:()=>Y("/shutdown",Me({})),metricas:()=>Y("/metrics"),leads:{listar:()=>Y("/leads"),criar:t=>Y("/leads",Me(t)),atualizar:(t,e)=>Y(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>Y(`/leads/${t}`,{method:"DELETE"}),importar:t=>Y("/leads/import",Me({leads:t})),enriquecer:t=>Y(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>Y("/crm/leads"),criarLead:t=>Y("/crm/leads",Me(t)),moverLead:(t,e)=>Y(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>Y("/crm/sugestoes"),aplicar:(t,e)=>Y("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>Y("/campaigns"),criar:t=>Y("/campaigns",Me(t)),iniciar:t=>Y(`/campaigns/${t}/start`,Me({})),pausar:t=>Y(`/campaigns/${t}/pause`,Me({})),parar:t=>Y(`/campaigns/${t}/stop`,Me({})),remover:t=>Y(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>Y("/extractors/maps",Me(t)),instagram:(t,e)=>Y("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>Y("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>Y("/extract/cnpj/search",Me(t)),cnpjExtract:t=>Y("/extractors/cnpj",Me(t))},config:{obter:()=>Y("/config"),salvar:t=>Y("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>Y("/config/test/llm",Me({})),testarIntegracao:t=>Y("/config/test/integracao",Me({url:t}))},integracoes:()=>Y("/integrations/status"),agentes:{templates:()=>Y("/agents/templates"),salvarTemplates:t=>Y("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>Y("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>Y("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>Y("/webhook"),salvar:t=>Y("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>Y("/webhook/test",Me({url:t}))}};function Q(...t){return t.filter(Boolean).join(" ")}function a_(t){return(t||"?").trim().charAt(0).toUpperCase()||"?"}function Il(t){if(!t)return"—";try{return new Date(t).toLocaleString("pt-BR",{day:"2-digit",month:"2-digit",year:"2-digit",hour:"2-digit",minute:"2-digit"})}catch{return t}}function na(t){const e=(t||"").replace(/\D/g,"");return e?e.length===11?`(${e.slice(0,2)}) ${e.slice(2,7)}-${e.slice(7)}`:e.length===10?`(${e.slice(0,2)}) ${e.slice(2,6)}-${e.slice(6)}`:t||"—":"—"}function l_(t){const e=["nome","telefone","email","empresa","cidade","observacoes"],n=e.filter(s=>String(t[s]||"").trim().length>0);return Math.round(n.length/e.length*100)}function c_(t){const e={nome:"nome",telefone:"telefone",email:"e-mail",empresa:"empresa",cidade:"cidade",observacoes:"observações"};return["nome","telefone","email","empresa","cidade","observacoes"].filter(s=>String(t[s]||"").trim().length===0).map(s=>e[s])}function u_(t,e){if(t>=100)return"Lead 100% completo — excelente! 🎉";const n=e.slice(0,2).join(" e ");return`Faltam ${100-t}% (${n}${e.length>2?"...":""}) para o lead ficar completo`}function d_(t,e){const n=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),s=URL.createObjectURL(n),r=document.createElement("a");r.href=s,r.download=e,r.click(),URL.revokeObjectURL(s)}const f_={md:"max-w-lg",lg:"max-w-2xl",xl:"max-w-4xl"};function Xt({aberto:t,onFechar:e,titulo:n,children:s,icone:r="fas fa-circle-info",largura:i="md"}){return w.useEffect(()=>{const o=a=>{a.key==="Escape"&&e()};return t&&window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[t,e]),t?u.jsx("div",{className:"fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4",onMouseDown:o=>{o.target===o.currentTarget&&e()},children:u.jsxs("div",{className:Q("bg-white rounded-2xl shadow-xl w-full",f_[i]),children:[u.jsxs("div",{className:"flex items-center justify-between px-6 py-4 border-b border-gray-100",children:[u.jsxs("h3",{className:"text-base font-semibold text-gray-800 flex items-center gap-2",children:[u.jsx("i",{className:Q(r,"text-indigo-500")})," ",n]}),u.jsx("button",{onClick:e,className:"text-gray-400 hover:text-gray-600","aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]}),u.jsx("div",{className:"px-6 py-5 max-h-[70vh] overflow-y-auto",children:s})]})}):null}const h_=[{icone:"fas fa-users",texto:"Cadastre leads manualmente ou importe pelos Extratores (Maps, Instagram, CNPJ)."},{icone:"fas fa-columns",texto:"Arraste os cards no CRM conforme avança no funil (Leads → Em contato → Proposta → Fechado)."},{icone:"fas fa-bullhorn",texto:"Monte campanhas de disparo com seus templates e acompanhe as taxas no Painel."},{icone:"fas fa-robot",texto:"Use os Agentes (disparo e atendimento) e configure o LLM em Ferramentas para turbiná-los."}];function p_({titulo:t,nomeUsuario:e}){const[n,s]=w.useState(!1);return u.jsxs("header",{className:"bg-white shadow-sm px-8 py-4 flex items-center justify-between shrink-0",children:[u.jsx("div",{children:u.jsx("h2",{className:"text-lg font-semibold text-gray-800",children:t})}),u.jsxs("div",{className:"flex items-center space-x-4",children:[u.jsxs("button",{onClick:()=>s(!0),className:"text-sm text-indigo-600 hover:text-indigo-800 font-medium",children:[u.jsx("i",{className:"fas fa-play-circle mr-1"})," Ver tutorial"]}),u.jsx("div",{className:"w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-bold text-sm",children:a_(e)}),u.jsx("span",{className:"text-sm font-medium text-gray-700",children:e})]}),u.jsxs(Xt,{aberto:n,onFechar:()=>s(!1),titulo:"Tutorial rápido",icone:"fas fa-graduation-cap",children:[u.jsx("ol",{className:"space-y-4",children:h_.map((r,i)=>u.jsxs("li",{className:"flex gap-3 items-start",children:[u.jsx("span",{className:"w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center shrink-0",children:u.jsx("i",{className:r.icone})}),u.jsx("p",{className:"text-sm text-gray-600 pt-1.5",children:r.texto})]},i))}),u.jsx("p",{className:"text-xs text-gray-400 mt-5",children:"Dica: o LLM (para enriquecimento e agentes) é configurado em Ferramentas."})]})]})}const g_=[{itens:[{para:"/",icone:"fas fa-chart-pie text-indigo-300",rotulo:"Painel",fim:!0}]},{secao:"Conexões",itens:[{para:"/leads",icone:"fas fa-users",rotulo:"Leads"},{para:"/agentes",icone:"fas fa-robot",rotulo:"Agentes"},{para:"/ferramentas",icone:"fas fa-tools",rotulo:"Ferramentas"}]},{itens:[{para:"/extrator-maps",icone:"fas fa-map-marked-alt",rotulo:"Extrator Maps"},{para:"/extrator-instagram",icone:"fab fa-instagram",rotulo:"Extrator Instagram"},{para:"/extrator-cnpj",icone:"fas fa-building",rotulo:"Extrator CNPJ"}]},{secao:"Gestão",itens:[{para:"/disparos",icone:"fas fa-bullhorn",rotulo:"Disparos"},{para:"/crm",icone:"fas fa-columns",rotulo:"CRM"}]},{secao:"Integrações",itens:[{para:"/webhook",icone:"fas fa-link",rotulo:"Webhook"},{para:"/suporte",icone:"fas fa-headset",rotulo:"Suporte"}]}];function m_({onSair:t}){const e=Ts(),[n,s]=w.useState(!1);w.useEffect(()=>{e.pathname==="/agentes"&&s(!0)},[e.pathname]);const r=i=>{if(i.rotulo==="Agentes")return e.pathname==="/agentes";const[o,a]=i.para.split("?");return a?e.pathname===o&&e.search.includes(a):i.fim?e.pathname===o:e.pathname.startsWith(o)};return u.jsxs("aside",{className:"w-64 bg-[#1e1b4b] text-white flex flex-col shrink-0 h-screen sticky top-0",children:[u.jsx("div",{className:"p-6 border-b border-gray-700",children:u.jsxs("h1",{className:"text-2xl font-bold tracking-tight flex items-center gap-2",children:[u.jsx("span",{className:"text-2xl",children:"⚡"}),"LeadMaster",u.jsxs("span",{className:"text-[10px] font-medium text-indigo-300 border border-indigo-400/40 rounded-md px-1.5 py-0.5",children:["v","0.1.15"]})]})}),u.jsx("nav",{className:"flex-1 overflow-y-auto py-4",children:u.jsx("ul",{className:"space-y-1",children:g_.map((i,o)=>u.jsx("li",{children:u.jsxs("ul",{children:[i.secao&&u.jsx("li",{className:"px-6 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider list-none",children:i.secao}),i.itens.map(a=>a.rotulo==="Agentes"?u.jsxs("li",{children:[u.jsxs("button",{type:"button",onClick:()=>s(l=>!l),className:Q("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:Q("fas fa-chevron-down ml-auto text-xs text-gray-400 transition-transform",n&&"rotate-180")})]}),n&&u.jsxs("ul",{className:"pl-12 space-y-1 mt-1",children:[u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=disparo",rotulo:"Agente de Disparo",ativo:e.pathname==="/agentes"&&e.search.includes("disparo")})}),u.jsx("li",{children:u.jsx(Yp,{para:"/agentes?tab=atendimento",rotulo:"Agente de Atendimento",ativo:e.pathname==="/agentes"&&e.search.includes("atendimento")})})]})]},"agentes"):u.jsx("li",{children:u.jsxs(gy,{to:a.para,className:Q("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:Q(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo})]})},a.para))]})},o))})}),u.jsx("div",{className:"p-4 border-t border-gray-700",children:u.jsxs("button",{onClick:t,className:"w-full flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg text-sm font-medium hover:opacity-90 transition",children:[u.jsx("i",{className:"fas fa-sign-out-alt mr-2"})," Sair"]})})]})}function Yp({para:t,rotulo:e,ativo:n}){return u.jsx(gy,{to:t,className:Q("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=w.createContext({toast:()=>{}});function en(){return w.useContext(my)}const x_={sucesso:"bg-green-600",erro:"bg-red-600",info:"bg-indigo-600"},y_={sucesso:"fas fa-check-circle",erro:"fas fa-exclamation-circle",info:"fas fa-info-circle"};function v_({children:t}){const[e,n]=w.useState([]),s=w.useRef(0),r=w.useCallback((i,o)=>{const a=++s.current;n(l=>[...l,{id:a,tipo:i,mensagem:o}]),setTimeout(()=>n(l=>l.filter(c=>c.id!==a)),4500)},[]);return u.jsxs(my.Provider,{value:{toast:r},children:[t,u.jsx("div",{className:"fixed top-4 right-4 z-[100] space-y-2 w-80",children:e.map(i=>u.jsxs("div",{className:Q(x_[i.tipo],"text-white text-sm rounded-lg shadow-lg px-4 py-3 flex items-start gap-2 animate-pulse-once"),role:"status",children:[u.jsx("i",{className:Q(y_[i.tipo],"mt-0.5")}),u.jsx("span",{className:"flex-1",children:i.mensagem}),u.jsx("button",{className:"opacity-70 hover:opacity-100",onClick:()=>n(o=>o.filter(a=>a.id!==i.id)),"aria-label":"Fechar",children:u.jsx("i",{className:"fas fa-times"})})]},i.id))})]})}function Pa({rotulo:t,valor:e,subtitulo:n,children:s,icone:r}){return u.jsxs("div",{className:"bg-white rounded-xl shadow-sm p-5 border border-gray-100",children:[u.jsxs("div",{className:"flex items-center justify-between",children:[u.jsx("p",{className:"text-sm text-gray-500 font-medium",children:t}),r&&u.jsx("i",{className:`${r} text-indigo-300`})]}),s??u.jsxs(u.Fragment,{children:[u.jsx("p",{className:"text-2xl font-bold text-gray-800 mt-1",children:e}),n&&u.jsx("p",{className:"text-xs text-gray-400 mt-0.5",children:n})]})]})}/*!
68
68
  * @kurkle/color v0.3.4
69
69
  * https://github.com/kurkle/color#readme
70
70
  * (c) 2024 Jukka Kurkela
@@ -5,7 +5,7 @@
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>
8
+ <script type="module" crossorigin src="/assets/index-CAvEEMWg.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Bf28IekR.css">
10
10
  </head>
11
11
  <body class="bg-gray-100 min-h-screen">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/leadmaster",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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;