@danhachuel/leadmaster 0.1.16 → 0.1.17

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 (22) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/backend/__pycache__/main.cpython-311.pyc +0 -0
  3. package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
  4. package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
  5. package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
  6. package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
  7. package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
  8. package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
  9. package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
  10. package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
  11. package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  12. package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  13. package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  14. package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  15. package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
  16. package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  17. package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
  18. package/backend/tests/test_launcher_protections.py +13 -0
  19. package/frontend/dist/assets/{index-BTNs0Fhp.js → index-BWyDEt5a.js} +1 -1
  20. package/frontend/dist/index.html +1 -1
  21. package/package.json +1 -1
  22. package/scripts/cli.mjs +66 -5
package/CHANGELOG.md CHANGED
@@ -37,6 +37,31 @@ 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.17] — 2026-11-09
41
+
42
+ ### Corrigido
43
+
44
+ - **Erro `EBUSY` do npx — migração para Runtime Permanente.** A 0.1.15 tentou
45
+ resolver com kill switch dentro do launcher, o que é impossível: o `EBUSY`
46
+ acontece no gerenciador de arquivos do `npx` (`_npx\...`) **antes** de o
47
+ `cli.mjs` ser carregado, então o kill switch nunca rodava. Causa raiz: o
48
+ `uvicorn` era spawnado com `cwd` dentro do cache do npx, segurando o
49
+ diretório. Nova arquitetura:
50
+ - Pasta permanente **`<home>/.runtime`** guarda `backend/` + `frontend/dist/`.
51
+ - `install`: kill switch (libera arquivos) → apaga o `.runtime` anterior →
52
+ copia esta versão do pacote para ele. O cache do npx nunca mais é tocado
53
+ por processos nossos.
54
+ - `start`: spawna o Python a partir de **`<home>/.runtime/backend`** (nunca
55
+ de `__dirname`); sem `.runtime` pronto, auto-aprovisiona; com pronto,
56
+ reutiliza sem recopiar.
57
+ - Launcher faz `process.chdir(home)` na entrada — cwd fora do cache.
58
+ - Regra inviolável documentada no AGENTS.md ("terminantemente proibido
59
+ executar o servidor a partir da pasta temporária do npx") + gate de
60
+ regressão (`.runtime`, `copiarParaRuntime`, `cpSync`, `rmSync`,
61
+ `path.join(runtime, 'backend')`, `process.chdir(homeLauncher)`).
62
+ - `.runtime/` adicionado ao `.gitignore`; `doctor` reporta o status do
63
+ runtime.
64
+
40
65
  ## [0.1.16] — 2026-11-09
41
66
 
42
67
  ### Adicionado
@@ -78,6 +78,19 @@ class TestLauncherProtections:
78
78
  assert "já está rodando" not in _conteudo().lower()
79
79
  assert "ja esta rodando" not in _conteudo().lower()
80
80
 
81
+ def test_runtime_permanente(self):
82
+ # Arquitetura anti-EBUSY: executar de <home>/.runtime, nunca do cache npx.
83
+ c = _conteudo()
84
+ assert ".runtime" in c
85
+ assert "copiarParaRuntime" in c
86
+ assert "runtimeDir" in c
87
+ assert "cpSync" in c # cópia recursiva para o runtime
88
+ assert "rmSync" in c # limpeza do runtime anterior
89
+ # O spawn do servidor usa o runtime, não o PKG_ROOT.
90
+ assert "path.join(runtime, 'backend')" in c
91
+ # O launcher sai do cache do npx logo na entrada.
92
+ assert "process.chdir(homeLauncher)" in c
93
+
81
94
  def test_sigterm_nao_absorvido(self):
82
95
  # SIGTERM/SIGBREAK são paradas legítimas: sempre encerram.
83
96
  c = _conteudo()
@@ -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=S.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=S.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=S.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=S.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return S.useLayoutEffect(()=>o.listen(d),[o,d]),S.useEffect(()=>BS(s),[s]),S.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_=S.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}=S.useContext(Rs),m,g=!1;if(typeof c=="string"&&ZS.test(c)&&(m=c,JS))try{let v=new URL(window.location.href),_=c.startsWith("//")?new URL(v.protocol+c):new URL(c),w=pi(_.pathname,p);_.origin===v.origin&&w!=null?c=w+_.search+_.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 x(v){s&&s(v),v.defaultPrevented||y(v)}return S.createElement("a",Al({},h,{href:m||b,onClick:g||i?s:x,ref:n,target:l}))}),gy=S.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=Os(),m=S.useContext(ly),{navigator:g,basename:b}=S.useContext(Rs),y=m!=null&&r_(h)&&c===!0,x=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,_=m&&m.navigation&&m.navigation.location?m.navigation.location.pathname:null;r||(v=v.toLowerCase(),_=_?_.toLowerCase():null,x=x.toLowerCase()),_&&b&&(_=pi(_,b)||_);const w=x!=="/"&&x.endsWith("/")?x.length-1:x.length;let k=v===x||!o&&v.startsWith(x)&&v.charAt(w)==="/",C=_!=null&&(_===x||!o&&_.startsWith(x)&&_.charAt(x.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},M=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 P=typeof a=="function"?a(j):a;return S.createElement(e_,Al({},f,{"aria-current":M,className:E,ref:n,style:P,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=S.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=Os(),d=vc(t,{relative:o});return S.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=S.useRef(Rd(t)),n=S.useRef(!1),s=Os(),r=S.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=S.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=S.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);B(this,"status");this.status=n,this.name="ApiError"}}async function U(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:()=>U("/health"),doctor:()=>U("/doctor"),shutdown:()=>U("/shutdown",Me({})),metricas:()=>U("/metrics"),leads:{listar:()=>U("/leads"),criar:t=>U("/leads",Me(t)),atualizar:(t,e)=>U(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>U(`/leads/${t}`,{method:"DELETE"}),importar:t=>U("/leads/import",Me({leads:t})),enriquecer:t=>U(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>U("/crm/leads"),criarLead:t=>U("/crm/leads",Me(t)),moverLead:(t,e)=>U(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>U("/crm/sugestoes"),aplicar:(t,e)=>U("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>U("/campaigns"),criar:t=>U("/campaigns",Me(t)),status:(t,e)=>U(`/campaigns/${t}/status`,{method:"PUT",body:JSON.stringify({status:e})}),iniciar:t=>U(`/campaigns/${t}/start`,Me({})),pausar:t=>U(`/campaigns/${t}/pause`,Me({})),parar:t=>U(`/campaigns/${t}/stop`,Me({})),remover:t=>U(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>U("/extractors/maps",Me(t)),instagram:(t,e)=>U("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>U("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>U("/extract/cnpj/search",Me(t)),cnpjExtract:t=>U("/extractors/cnpj",Me(t))},config:{obter:()=>U("/config"),salvar:t=>U("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>U("/config/test/llm",Me({})),testarIntegracao:t=>U("/config/test/integracao",Me({url:t}))},integracoes:()=>U("/integrations/status"),agentes:{templates:()=>U("/agents/templates"),salvarTemplates:t=>U("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>U("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>U("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>U("/webhook"),salvar:t=>U("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>U("/webhook/test",Me({url:t}))}};function H(...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 S.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:H("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:H(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]=S.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=Os(),[n,s]=S.useState(!1);S.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.16"]})]})}),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:H("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:H(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:H("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:H("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:H(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:H("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=S.createContext({toast:()=>{}});function en(){return S.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]=S.useState([]),s=S.useRef(0),r=S.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:H(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:H(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=S.createContext({isTransitioning:!1}),qS="startTransition",Qp=ib[qS];function GS(t){let{basename:e,children:n,future:s,window:r}=t,i=S.useRef();i.current==null&&(i.current=tS({window:r,v5Compat:!0}));let o=i.current,[a,l]=S.useState({action:o.action,location:o.location}),{v7_startTransition:c}=s||{},d=S.useCallback(f=>{c&&Qp?Qp(()=>l(f)):l(f)},[l,c]);return S.useLayoutEffect(()=>o.listen(d),[o,d]),S.useEffect(()=>BS(s),[s]),S.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_=S.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}=S.useContext(Rs),m,g=!1;if(typeof c=="string"&&ZS.test(c)&&(m=c,JS))try{let v=new URL(window.location.href),_=c.startsWith("//")?new URL(v.protocol+c):new URL(c),w=pi(_.pathname,p);_.origin===v.origin&&w!=null?c=w+_.search+_.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 x(v){s&&s(v),v.defaultPrevented||y(v)}return S.createElement("a",Al({},h,{href:m||b,onClick:g||i?s:x,ref:n,target:l}))}),gy=S.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=Os(),m=S.useContext(ly),{navigator:g,basename:b}=S.useContext(Rs),y=m!=null&&r_(h)&&c===!0,x=g.encodeLocation?g.encodeLocation(h).pathname:h.pathname,v=p.pathname,_=m&&m.navigation&&m.navigation.location?m.navigation.location.pathname:null;r||(v=v.toLowerCase(),_=_?_.toLowerCase():null,x=x.toLowerCase()),_&&b&&(_=pi(_,b)||_);const w=x!=="/"&&x.endsWith("/")?x.length-1:x.length;let k=v===x||!o&&v.startsWith(x)&&v.charAt(w)==="/",C=_!=null&&(_===x||!o&&_.startsWith(x)&&_.charAt(x.length)==="/"),j={isActive:k,isPending:C,isTransitioning:y},M=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 P=typeof a=="function"?a(j):a;return S.createElement(e_,Al({},f,{"aria-current":M,className:E,ref:n,style:P,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=S.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=Os(),d=vc(t,{relative:o});return S.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=S.useRef(Rd(t)),n=S.useRef(!1),s=Os(),r=S.useMemo(()=>VS(s.search,n.current?null:e.current),[s.search]),i=bi(),o=S.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=S.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);B(this,"status");this.status=n,this.name="ApiError"}}async function U(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:()=>U("/health"),doctor:()=>U("/doctor"),shutdown:()=>U("/shutdown",Me({})),metricas:()=>U("/metrics"),leads:{listar:()=>U("/leads"),criar:t=>U("/leads",Me(t)),atualizar:(t,e)=>U(`/leads/${t}`,{method:"PATCH",body:JSON.stringify(e)}),remover:t=>U(`/leads/${t}`,{method:"DELETE"}),importar:t=>U("/leads/import",Me({leads:t})),enriquecer:t=>U(`/leads/${t}/enrich`,Me({}))},crm:{leadsListar:()=>U("/crm/leads"),criarLead:t=>U("/crm/leads",Me(t)),moverLead:(t,e)=>U(`/crm/leads/${t}/etapa`,{method:"PUT",body:JSON.stringify({etapa:e})}),sugestoes:()=>U("/crm/sugestoes"),aplicar:(t,e)=>U("/crm/sugestoes/aplicar",Me({lead_id:t,para:e}))},campanhas:{listar:()=>U("/campaigns"),criar:t=>U("/campaigns",Me(t)),status:(t,e)=>U(`/campaigns/${t}/status`,{method:"PUT",body:JSON.stringify({status:e})}),iniciar:t=>U(`/campaigns/${t}/start`,Me({})),pausar:t=>U(`/campaigns/${t}/pause`,Me({})),parar:t=>U(`/campaigns/${t}/stop`,Me({})),remover:t=>U(`/campaigns/${t}`,{method:"DELETE"})},extrair:{maps:t=>U("/extractors/maps",Me(t)),instagram:(t,e)=>U("/extractors/instagram",Me({profile_url:t,limit:e})),cnpj:t=>U("/extract/cnpj",Me({cnpj:t})),cnpjBusca:t=>U("/extract/cnpj/search",Me(t)),cnpjExtract:t=>U("/extractors/cnpj",Me(t))},config:{obter:()=>U("/config"),salvar:t=>U("/config",{method:"PUT",body:JSON.stringify(t)}),testarLlm:()=>U("/config/test/llm",Me({})),testarIntegracao:t=>U("/config/test/integracao",Me({url:t}))},integracoes:()=>U("/integrations/status"),agentes:{templates:()=>U("/agents/templates"),salvarTemplates:t=>U("/agents/templates",{method:"PUT",body:JSON.stringify(t)}),testarTemplate:(t,e)=>U("/agents/templates/test",Me({template:t,lead_id:e})),chat:t=>U("/agents/attendance/chat",Me({mensagens:t}))},webhook:{obter:()=>U("/webhook"),salvar:t=>U("/webhook",{method:"PUT",body:JSON.stringify(t)}),testar:t=>U("/webhook/test",Me({url:t}))}};function H(...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 S.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:H("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:H(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]=S.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=Os(),[n,s]=S.useState(!1);S.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.17"]})]})}),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:H("sidebar-link w-full flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:H(a.icone,"w-5 text-gray-400")}),u.jsx("span",{className:"ml-3",children:a.rotulo}),u.jsx("i",{className:H("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:H("sidebar-link flex items-center px-6 py-2.5 text-sm",r(a)&&"active"),children:[u.jsx("i",{className:H(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:H("sidebar-link block px-4 py-2 text-xs text-gray-300 rounded",n&&"active"),children:e})}const my=S.createContext({toast:()=>{}});function en(){return S.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]=S.useState([]),s=S.useRef(0),r=S.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:H(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:H(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-BTNs0Fhp.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BWyDEt5a.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BKo_OphM.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.16",
3
+ "version": "0.1.17",
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, execSync } from 'node:child_process';
26
- import { existsSync, mkdirSync, readFileSync, appendFileSync } from 'node:fs';
26
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, rmSync, cpSync } 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';
@@ -87,6 +87,41 @@ function ensureHome(home) {
87
87
  }
88
88
  }
89
89
 
90
+ // ---------------------------------------------------------------------------
91
+ // Runtime permanente (regra inviolável do AGENTS.md)
92
+ //
93
+ // O servidor NUNCA executa a partir do cache do npx (`_npx\...`): um processo
94
+ // rodando de lá segura o diretório e o npx falha com EBUSY na próxima
95
+ // instalação — antes mesmo de qualquer código do launcher rodar (por isso o
96
+ // kill switch sozinho não resolvia). Solução: `install` copia backend/ e
97
+ // frontend/dist/ para <home>/.runtime e `start` executa de lá. O cache do npx
98
+ // nunca é tocado por processos nossos.
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function runtimeDir(home) {
102
+ return path.join(home, '.runtime');
103
+ }
104
+
105
+ function runtimePronto(home) {
106
+ return existsSync(path.join(runtimeDir(home), 'backend', 'main.py'));
107
+ }
108
+
109
+ /** Copia backend/ e frontend/dist/ do pacote para o runtime permanente. */
110
+ function copiarParaRuntime(home) {
111
+ const runtime = runtimeDir(home);
112
+ rmSync(runtime, { recursive: true, force: true });
113
+ mkdirSync(runtime, { recursive: true });
114
+ cpSync(path.join(PKG_ROOT, 'backend'), path.join(runtime, 'backend'), {
115
+ recursive: true,
116
+ filter: (src) => !src.includes('__pycache__') && !src.endsWith('.pyc') && !src.includes(`${path.sep}.venv`),
117
+ });
118
+ const dist = path.join(PKG_ROOT, 'frontend', 'dist');
119
+ if (existsSync(path.join(dist, 'index.html'))) {
120
+ cpSync(dist, path.join(runtime, 'frontend', 'dist'), { recursive: true });
121
+ }
122
+ return runtime;
123
+ }
124
+
90
125
  // ---------------------------------------------------------------------------
91
126
  // Python
92
127
  // ---------------------------------------------------------------------------
@@ -471,6 +506,14 @@ async function cmdInstall(args) {
471
506
  // Frontend (modo repositório): idempotente, só instala o que faltar.
472
507
  instalarModoFonte();
473
508
 
509
+ // Runtime permanente: reutiliza a porta inicial já validada pelo kill switch
510
+ // acima, apaga o .runtime anterior (arquivos liberados) e copia esta versão
511
+ // do pacote para lá. Daqui em diante o servidor NUNCA roda de dentro do
512
+ // cache do npx — o EBUSY acaba.
513
+ await encerrarInstanciaAnterior(portaInicial);
514
+ const runtime = copiarParaRuntime(home);
515
+ console.log(`✓ Runtime permanente preparado: ${runtime}`);
516
+
474
517
  console.log('');
475
518
  console.log('Instalação concluída. Para usar (comando único):');
476
519
  console.log(' npx --yes @danhachuel/leadmaster@latest');
@@ -524,20 +567,28 @@ function cmdStart(args) {
524
567
 
525
568
  // Comando único (estilo Thunderbolt): o start também se auto-prepara.
526
569
  // Modo fonte sem deps/build? Instala só o que faltar antes de subir.
527
- // Pacote npx: UI já vem pronta (dist) e nada disto roda.
528
570
  if (existsSync(path.join(PKG_ROOT, 'frontend', 'package.json'))) {
529
571
  instalarModoFonte();
530
- } else if (!existsSync(path.join(PKG_ROOT, 'frontend', 'dist', 'index.html'))) {
531
- console.warn('⚠ Interface construída não encontrada (frontend/dist).');
532
572
  }
533
573
 
574
+ // Kill switch: se uma instância ANTIGA (runtime anterior) ainda ocupar a
575
+ // porta, ela é encerrada antes da nova assumir — nunca reutilizamos.
576
+ await encerrarInstanciaAnterior(portaInicial);
577
+
578
+ // Runtime permanente (regra inviolável): o servidor NUNCA roda de dentro
579
+ // do cache do npx (EBUSY na próxima instalação). Sem runtime pronto, o
580
+ // start se auto-aprovisiona copiando para <home>/.runtime; com runtime
581
+ // pronto, reutiliza (sem recopiar nada).
582
+ const runtime = runtimePronto(home) ? runtimeDir(home) : copiarParaRuntime(home);
583
+ console.log(`✓ Executando a partir do runtime permanente: ${runtime}`);
584
+
534
585
  // Workers nascem em grupo de processos próprio (detached: true =
535
586
  // CREATE_NEW_PROCESS_GROUP no Windows): sinais de Ctrl+C do terminal
536
587
  // (MobaXterm, VS Code, perda de foco) matam apenas o launcher — o
537
588
  // servidor Python continua rodando e só é encerrado pelo kill()
538
589
  // controlado abaixo (ou sobrevive para a próxima execução detectar).
539
590
  const child = spawn(python.cmd, [...python.args, '-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(porta), '--no-access-log'], {
540
- cwd: path.join(PKG_ROOT, 'backend'),
591
+ cwd: path.join(runtime, 'backend'),
541
592
  env: {
542
593
  ...process.env,
543
594
  LEADMASTER_HOME: home,
@@ -674,6 +725,7 @@ async function cmdDoctor(args) {
674
725
  console.log(`Servidor UI ........ ${ativaEm ? `rodando em http://127.0.0.1:${ativaEm}` : `parado (porta inicial ${portaInicial})`}`);
675
726
  const hasDist = existsSync(path.join(PKG_ROOT, 'frontend', 'dist', 'index.html'));
676
727
  console.log(`Interface (dist) ... ${hasDist ? 'presente' : 'ausente (modo dev ou rebuild necessário)'}`);
728
+ console.log(`Runtime permanente . ${runtimePronto(home) ? `pronto (${runtimeDir(home)})` : 'ausente — rode install'}`);
677
729
  console.log('');
678
730
  console.log('Diagnóstico concluído.');
679
731
  }
@@ -721,6 +773,15 @@ mkdirSync(LOG_DIR, { recursive: true });
721
773
  const CONSOLE_LOG = path.join(LOG_DIR, 'launcher_console.log');
722
774
  [process.stdout, process.stderr].forEach((stream) => guardConsoleStream(stream, CONSOLE_LOG));
723
775
 
776
+ // Nunca manter o cwd dentro do cache do npx: o launcher nasce com o cwd de
777
+ // onde o usuário rodou o comando; assim que sabemos o home, saímos de lá —
778
+ // nenhum processo nosso segura diretório do cache (fim do EBUSY no npx).
779
+ try {
780
+ process.chdir(homeLauncher);
781
+ } catch {
782
+ /* melhor esforço */
783
+ }
784
+
724
785
  switch (command) {
725
786
  case 'install':
726
787
  // cmdInstall é async (kill switch): a promise mantém o evento vivo até o