@danhachuel/leadmaster 0.0.0-stage → 0.1.0
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 +59 -0
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/backend/main.py +523 -0
- package/backend/requirements.txt +4 -0
- package/backend/services/__init__.py +1 -0
- package/backend/services/agents.py +235 -0
- package/backend/services/crm_agent.py +175 -0
- package/backend/services/extractors.py +322 -0
- package/backend/services/llm.py +189 -0
- package/backend/services/moneyprinter.py +102 -0
- package/backend/services/openmontage.py +62 -0
- package/backend/services/pipeline.py +515 -0
- package/backend/services/short_video_maker.py +72 -0
- package/backend/services/storage.py +460 -0
- package/backend/services/youtube_upload.py +163 -0
- package/frontend/dist/assets/fa-brands-400-D1LuMI3I.ttf +0 -0
- package/frontend/dist/assets/fa-brands-400-D_cYUPeE.woff2 +0 -0
- package/frontend/dist/assets/fa-regular-400-BjRzuEpd.woff2 +0 -0
- package/frontend/dist/assets/fa-regular-400-DZaxPHgR.ttf +0 -0
- package/frontend/dist/assets/fa-solid-900-CTAAxXor.woff2 +0 -0
- package/frontend/dist/assets/fa-solid-900-D0aA9rwL.ttf +0 -0
- package/frontend/dist/assets/fa-v4compatibility-C9RhG_FT.woff2 +0 -0
- package/frontend/dist/assets/fa-v4compatibility-CCth-dXg.ttf +0 -0
- package/frontend/dist/assets/index-CANBU7DH.js +93 -0
- package/frontend/dist/assets/index-eswbuT3S.css +5 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/index.html +14 -0
- package/package.json +49 -4
- package/scripts/cli.mjs +512 -0
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* LeadMaster — SaaS local de prospecção e automação de conteúdo.
|
|
4
|
+
* Launcher oficial do pacote NPM (@danhachuel/leadmaster).
|
|
5
|
+
*
|
|
6
|
+
* Comandos:
|
|
7
|
+
* install Prepara a pasta de dados (home) e instala as dependências Python.
|
|
8
|
+
* start Sobe o backend FastAPI (serving da UI construída) e abre o navegador.
|
|
9
|
+
* `npx @danhachuel/leadmaster` sem argumentos executa este comando.
|
|
10
|
+
* doctor Diagnóstico rápido: Node, Python, dependências, pasta de dados.
|
|
11
|
+
* help Ajuda.
|
|
12
|
+
* version Versão do pacote.
|
|
13
|
+
*
|
|
14
|
+
* Flags:
|
|
15
|
+
* --home=<caminho> Pasta de dados (padrão: %USERPROFILE%\\Documents\\REPOSITORIO\\LeadMaster).
|
|
16
|
+
* --port=<numero> Porta inicial da UI/API (padrão: 5055; se ocupada, tenta 5056, 5057, ...).
|
|
17
|
+
* --skip-python-deps Não instala dependências Python (usado em CI/smoke-test).
|
|
18
|
+
*
|
|
19
|
+
* Ambiente:
|
|
20
|
+
* LEADMASTER_HOME Mesmo efeito de --home.
|
|
21
|
+
* LEADMASTER_PORT Porta inicial da UI/API (mesmo efeito de --port).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { homedir, platform } from 'node:os';
|
|
27
|
+
import net from 'node:net';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import http from 'node:http';
|
|
31
|
+
|
|
32
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
|
|
34
|
+
// Porta padrão da UI/API local; se estiver ocupada, o launcher avança
|
|
35
|
+
// automaticamente (5055 → 5056 → 5057 → ...) até encontrar uma porta livre.
|
|
36
|
+
const UI_PORT = 5055;
|
|
37
|
+
const UI_PORT_MAX_TENTATIVAS = 50;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Parsing de argumentos
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const args = { _: [], home: null, port: null, skipPythonDeps: false };
|
|
45
|
+
for (const raw of argv) {
|
|
46
|
+
if (raw === '--skip-python-deps') args.skipPythonDeps = true;
|
|
47
|
+
else if (raw.startsWith('--home=')) args.home = raw.slice('--home='.length);
|
|
48
|
+
else if (raw.startsWith('--port=')) args.port = Number.parseInt(raw.slice('--port='.length), 10);
|
|
49
|
+
else if (raw.startsWith('-')) console.warn(`Aviso: flag desconhecida "${raw}" ignorada.`);
|
|
50
|
+
else args._.push(raw);
|
|
51
|
+
}
|
|
52
|
+
if (process.env.LEADMASTER_HOME) args.home = args.home || process.env.LEADMASTER_HOME;
|
|
53
|
+
if (process.env.LEADMASTER_PORT) args.port = args.port || Number.parseInt(process.env.LEADMASTER_PORT, 10);
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Resolução da pasta de dados (home)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function resolveHome(args) {
|
|
62
|
+
if (args.home) return path.resolve(args.home);
|
|
63
|
+
const docs = path.join(homedir(), 'Documents');
|
|
64
|
+
if (existsSync(docs)) return path.join(docs, 'REPOSITORIO', 'LeadMaster');
|
|
65
|
+
return path.join(homedir(), '.leadmaster');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureHome(home) {
|
|
69
|
+
for (const dir of [home, path.join(home, 'storage')]) {
|
|
70
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Python
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function detectPython() {
|
|
79
|
+
const candidates = platform() === 'win32' ? ['python', 'py'] : ['python3', 'python'];
|
|
80
|
+
for (const cmd of candidates) {
|
|
81
|
+
const extra = cmd === 'py' ? ['-3'] : [];
|
|
82
|
+
const probe = spawnSync(cmd, [...extra, '--version'], { encoding: 'utf8', windowsHide: true });
|
|
83
|
+
if (probe.status === 0) return { cmd, args: extra };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pythonRun(python, args, opts = {}) {
|
|
89
|
+
return spawnSync(python.cmd, [...python.args, ...args], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
windowsHide: true,
|
|
92
|
+
...opts,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pythonDepsOk(python) {
|
|
97
|
+
const probe = pythonRun(python, ['-c', 'import fastapi, uvicorn, httpx, pydantic'], { stdio: 'pipe' });
|
|
98
|
+
return probe.status === 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function installPythonDeps(python) {
|
|
102
|
+
console.log('→ Instalando dependências Python (fastapi, uvicorn, httpx, pydantic)...');
|
|
103
|
+
const res = pythonRun(python, ['-m', 'pip', 'install', '-r', path.join(PKG_ROOT, 'backend', 'requirements.txt'), '--quiet', '--disable-pip-version-check'], {
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
});
|
|
106
|
+
if (res.status !== 0) {
|
|
107
|
+
console.error('');
|
|
108
|
+
console.error('✗ Falha ao instalar as dependências Python.');
|
|
109
|
+
console.error(' Comando manual equivalente:');
|
|
110
|
+
console.error(` ${python.cmd} ${python.args.join(' ')} -m pip install -r "${path.join(PKG_ROOT, 'backend', 'requirements.txt')}"`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log('✓ Dependências Python instaladas.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Frontend (modo repositório/fonte): instala deps Node e constrói a UI.
|
|
118
|
+
// No pacote publicado via npx a UI já vem pronta (frontend/dist) e este
|
|
119
|
+
// bloco é pulado automaticamente.
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const NPM = platform() === 'win32' ? 'npm.cmd' : 'npm';
|
|
123
|
+
|
|
124
|
+
function npmRun(args, cwd, opts = {}) {
|
|
125
|
+
return spawnSync(NPM, args, { cwd, stdio: 'inherit', shell: true, windowsHide: true, ...opts });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function instalarNodeDeps(cwd, rotulo) {
|
|
129
|
+
console.log(`→ Instalando dependências Node (${rotulo})...`);
|
|
130
|
+
let res = npmRun(['install', '--no-fund', '--no-audit'], cwd);
|
|
131
|
+
if (res.status !== 0) {
|
|
132
|
+
// npm >= 12 pode bloquear postinstall scripts (ex.: esbuild) por política;
|
|
133
|
+
// aprova o pacote e repete a instalação.
|
|
134
|
+
console.log('ℹ npm bloqueou um postinstall; aprovando (esbuild) e repetindo a instalação...');
|
|
135
|
+
npmRun(['install-scripts', 'approve', 'esbuild'], cwd, { stdio: 'ignore' });
|
|
136
|
+
res = npmRun(['install', '--no-fund', '--no-audit'], cwd);
|
|
137
|
+
}
|
|
138
|
+
if (res.status !== 0) {
|
|
139
|
+
console.error(`✗ Falha ao instalar as dependências Node (${rotulo}).`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
console.log(`✓ Dependências Node instaladas (${rotulo}).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Idempotente por design: NÃO reinstala tudo a cada versão.
|
|
147
|
+
* - Dependências Node presentes? Pula a instalação.
|
|
148
|
+
* - Interface (dist) já construída? Pula o build.
|
|
149
|
+
* - Build falhou (deps desatualizadas após troca de versão)?
|
|
150
|
+
* Roda npm install incremental e tenta UMA vez mais.
|
|
151
|
+
*/
|
|
152
|
+
function instalarFrontend() {
|
|
153
|
+
const frontend = path.join(PKG_ROOT, 'frontend');
|
|
154
|
+
const dist = path.join(frontend, 'dist', 'index.html');
|
|
155
|
+
const viteBin =
|
|
156
|
+
platform() === 'win32'
|
|
157
|
+
? path.join(frontend, 'node_modules', '.bin', 'vite.cmd')
|
|
158
|
+
: path.join(frontend, 'node_modules', '.bin', 'vite');
|
|
159
|
+
|
|
160
|
+
if (!existsSync(viteBin)) {
|
|
161
|
+
instalarNodeDeps(frontend, 'React, Vite, Tailwind');
|
|
162
|
+
} else {
|
|
163
|
+
console.log('✓ Dependências Node do frontend já instaladas — nada a fazer.');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (existsSync(dist)) {
|
|
167
|
+
console.log('✓ Interface já construída (frontend/dist) — nada a fazer.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log('→ Construindo a interface (npm run build)...');
|
|
172
|
+
let build = npmRun(['run', 'build'], frontend);
|
|
173
|
+
if (build.status !== 0 || !existsSync(dist)) {
|
|
174
|
+
console.log('ℹ Build falhou — atualizando dependências Node (só o que faltar) e repetindo...');
|
|
175
|
+
instalarNodeDeps(frontend, 'atualização incremental do frontend');
|
|
176
|
+
build = npmRun(['run', 'build'], frontend);
|
|
177
|
+
if (build.status !== 0 || !existsSync(dist)) {
|
|
178
|
+
console.error('✗ Falha ao construir a interface (frontend/dist).');
|
|
179
|
+
console.error(' Rode manualmente: npm --prefix frontend run build');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.log('✓ Interface pronta (frontend/dist).');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Modo fonte (checkout do repositório): prepara raiz + frontend + build,
|
|
188
|
+
* sempre de forma idempotente. No pacote npx a UI já vem pronta e isto
|
|
189
|
+
* é pulado automaticamente (não existe frontend/package.json no tarball).
|
|
190
|
+
*/
|
|
191
|
+
function instalarModoFonte() {
|
|
192
|
+
if (!existsSync(path.join(PKG_ROOT, 'frontend', 'package.json'))) return false;
|
|
193
|
+
console.log('ℹ Modo repositório (fonte) detectado.');
|
|
194
|
+
// Raiz: devDependencies (concurrently, usadas por "npm run dev").
|
|
195
|
+
if (!existsSync(path.join(PKG_ROOT, 'node_modules', 'concurrently'))) {
|
|
196
|
+
const res = npmRun(['install', '--no-fund', '--no-audit'], PKG_ROOT);
|
|
197
|
+
if (res.status !== 0) {
|
|
198
|
+
console.warn('⚠ Falha nas dependências da raiz (npm run dev pode não funcionar; o painel em si não é afetado).');
|
|
199
|
+
} else {
|
|
200
|
+
console.log('✓ Dependências da raiz instaladas.');
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
console.log('✓ Dependências da raiz já instaladas — nada a fazer.');
|
|
204
|
+
}
|
|
205
|
+
instalarFrontend();
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// HTTP helpers
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function httpGetJson(url, timeoutMs = 1500) {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
const req = http.get(url, { timeout: timeoutMs }, (res) => {
|
|
216
|
+
let body = '';
|
|
217
|
+
res.on('data', (c) => (body += c));
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
try {
|
|
220
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: JSON.parse(body) });
|
|
221
|
+
} catch {
|
|
222
|
+
resolve({ ok: false, status: res.statusCode, body: null });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, status: 0, body: null }); });
|
|
227
|
+
req.on('error', () => resolve({ ok: false, status: 0, body: null }));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function waitForHealth(port, timeoutMs) {
|
|
232
|
+
const deadline = Date.now() + timeoutMs;
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
const res = await httpGetJson(`http://127.0.0.1:${port}/api/health`, 1000);
|
|
235
|
+
if (res.ok) return res.body;
|
|
236
|
+
await sleep(500);
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
242
|
+
|
|
243
|
+
function openBrowser(url) {
|
|
244
|
+
try {
|
|
245
|
+
if (platform() === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();
|
|
246
|
+
else if (platform() === 'darwin') spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
247
|
+
else spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
248
|
+
} catch {
|
|
249
|
+
console.log(`Abra manualmente: ${url}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function portaLivre(port) {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const server = net.createServer();
|
|
256
|
+
server.once('error', () => resolve(false));
|
|
257
|
+
server.once('listening', () => server.close(() => resolve(true)));
|
|
258
|
+
server.listen(port, '127.0.0.1');
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function resolverPortaUI(portaInicial) {
|
|
263
|
+
/**
|
|
264
|
+
* Varre portas a partir de portaInicial (padrão 5055):
|
|
265
|
+
* - se encontrar uma instância saudável do LeadMaster, devolve {jaRodando: true};
|
|
266
|
+
* - senão devolve a primeira porta livre {jaRodando: false, porta};
|
|
267
|
+
* - se nenhuma porta livre no intervalo, devolve {porta: null}.
|
|
268
|
+
*/
|
|
269
|
+
for (let i = 0; i < UI_PORT_MAX_TENTATIVAS; i++) {
|
|
270
|
+
const porta = portaInicial + i;
|
|
271
|
+
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 800);
|
|
272
|
+
if (saude.ok && saude.body && (saude.body.versao || saude.body.storage)) {
|
|
273
|
+
return { jaRodando: true, porta };
|
|
274
|
+
}
|
|
275
|
+
if (await portaLivre(porta)) {
|
|
276
|
+
return { jaRodando: false, porta };
|
|
277
|
+
}
|
|
278
|
+
console.log(`ℹ Porta ${porta} ocupada — tentando ${porta + 1}...`);
|
|
279
|
+
}
|
|
280
|
+
return { jaRodando: false, porta: null };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function procurarInstanciaAtiva(portaInicial, maxTentativas = 10) {
|
|
284
|
+
for (let i = 0; i < maxTentativas; i++) {
|
|
285
|
+
const porta = portaInicial + i;
|
|
286
|
+
const saude = await httpGetJson(`http://127.0.0.1:${porta}/api/health`, 800);
|
|
287
|
+
if (saude.ok && saude.body && (saude.body.versao || saude.body.storage)) {
|
|
288
|
+
return porta;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Comandos
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
function cmdInstall(args) {
|
|
299
|
+
const home = resolveHome(args);
|
|
300
|
+
console.log(`LeadMaster v${VERSION} — instalação local`);
|
|
301
|
+
console.log('');
|
|
302
|
+
ensureHome(home);
|
|
303
|
+
console.log(`✓ Pasta de dados pronta: ${home}`);
|
|
304
|
+
console.log(` (leads, campanhas, pipeline e configuração ficam em "${path.join(home, 'storage')}")`);
|
|
305
|
+
|
|
306
|
+
const python = detectPython();
|
|
307
|
+
if (!python) {
|
|
308
|
+
console.error('✗ Python 3.11+ não encontrado no PATH.');
|
|
309
|
+
console.error(' Instale em https://www.python.org/downloads/ (marque "Add python.exe to PATH").');
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
const versionOut = pythonRun(python, ['--version'], { stdio: 'pipe' }).stdout.trim();
|
|
313
|
+
console.log(`✓ Python encontrado: ${versionOut}`);
|
|
314
|
+
|
|
315
|
+
if (args.skipPythonDeps) {
|
|
316
|
+
console.log('ℹ Dependências Python não instaladas (--skip-python-deps).');
|
|
317
|
+
} else if (pythonDepsOk(python)) {
|
|
318
|
+
console.log('✓ Dependências Python já estão instaladas — nada a fazer.');
|
|
319
|
+
} else {
|
|
320
|
+
installPythonDeps(python);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Frontend (modo repositório): idempotente, só instala o que faltar.
|
|
324
|
+
instalarModoFonte();
|
|
325
|
+
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log('Instalação concluída. Para usar (comando único):');
|
|
328
|
+
console.log(` npx --yes @danhachuel/leadmaster@${VERSION}`);
|
|
329
|
+
console.log(' (o mesmo comando, sem "install", também verifica e prepara tudo sozinho)');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function cmdStart(args) {
|
|
333
|
+
const home = resolveHome(args);
|
|
334
|
+
const portaInicial = Number.isFinite(args.port) && args.port > 0 ? args.port : UI_PORT;
|
|
335
|
+
|
|
336
|
+
console.log(`LeadMaster v${VERSION} — iniciando ambiente local`);
|
|
337
|
+
console.log('');
|
|
338
|
+
|
|
339
|
+
ensureHome(home);
|
|
340
|
+
console.log(`✓ Pasta de dados: ${home}`);
|
|
341
|
+
|
|
342
|
+
(async () => {
|
|
343
|
+
// Resolve a porta: reusa instância ativa ou pega a primeira livre (5055 → 5056 → ...).
|
|
344
|
+
const { jaRodando, porta } = await resolverPortaUI(portaInicial);
|
|
345
|
+
if (porta === null) {
|
|
346
|
+
console.error(`✗ Nenhuma porta livre entre ${portaInicial} e ${portaInicial + UI_PORT_MAX_TENTATIVAS - 1}.`);
|
|
347
|
+
console.error(' Libere uma porta ou inicie com --port=<numero>.');
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
const url = `http://127.0.0.1:${porta}`;
|
|
351
|
+
if (jaRodando) {
|
|
352
|
+
console.log(`ℹ LeadMaster já está rodando em ${url} — abrindo o navegador.`);
|
|
353
|
+
openBrowser(url);
|
|
354
|
+
process.exit(0);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (porta !== portaInicial) {
|
|
358
|
+
console.log(`ℹ Porta ${portaInicial} ocupada — usando a porta ${porta}.`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const python = detectPython();
|
|
362
|
+
if (!python) {
|
|
363
|
+
console.error('✗ Python 3.11+ não encontrado no PATH. Rode primeiro: npx --yes @danhachuel/leadmaster install');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!pythonDepsOk(python)) {
|
|
368
|
+
if (args.skipPythonDeps) {
|
|
369
|
+
console.warn('⚠ Dependências Python ausentes e --skip-python-deps ativo; o servidor pode não subir.');
|
|
370
|
+
} else {
|
|
371
|
+
installPythonDeps(python);
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
console.log('✓ Dependências Python OK.');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Comando único (estilo Thunderbolt): o start também se auto-prepara.
|
|
378
|
+
// Modo fonte sem deps/build? Instala só o que faltar antes de subir.
|
|
379
|
+
// Pacote npx: UI já vem pronta (dist) e nada disto roda.
|
|
380
|
+
if (existsSync(path.join(PKG_ROOT, 'frontend', 'package.json'))) {
|
|
381
|
+
instalarModoFonte();
|
|
382
|
+
} else if (!existsSync(path.join(PKG_ROOT, 'frontend', 'dist', 'index.html'))) {
|
|
383
|
+
console.warn('⚠ Interface construída não encontrada (frontend/dist).');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const child = spawn(python.cmd, [...python.args, '-m', 'uvicorn', 'main:app', '--host', '127.0.0.1', '--port', String(porta)], {
|
|
387
|
+
cwd: path.join(PKG_ROOT, 'backend'),
|
|
388
|
+
env: {
|
|
389
|
+
...process.env,
|
|
390
|
+
LEADMASTER_HOME: home,
|
|
391
|
+
LEADMASTER_PORT: String(porta),
|
|
392
|
+
LEADMASTER_VERSION: VERSION,
|
|
393
|
+
PYTHONUTF8: '1',
|
|
394
|
+
PYTHONIOENCODING: 'utf-8',
|
|
395
|
+
},
|
|
396
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
397
|
+
windowsHide: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const prefix = (buf, tag) =>
|
|
401
|
+
buf
|
|
402
|
+
.toString('utf8')
|
|
403
|
+
.split(/\r?\n/)
|
|
404
|
+
.filter(Boolean)
|
|
405
|
+
.forEach((line) => console.log(`[${tag}] ${line}`));
|
|
406
|
+
child.stdout.on('data', (d) => prefix(d, 'api'));
|
|
407
|
+
child.stderr.on('data', (d) => prefix(d, 'api'));
|
|
408
|
+
|
|
409
|
+
child.on('exit', (code) => {
|
|
410
|
+
console.log(code === 0 ? '\nLeadMaster encerrado. Até já!' : `\n✗ Servidor local encerrou com código ${code}.`);
|
|
411
|
+
process.exit(code === null ? 1 : code);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const stop = () => {
|
|
415
|
+
console.log('\nEncerrando LeadMaster...');
|
|
416
|
+
if (platform() === 'win32' && child.pid) {
|
|
417
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
|
|
418
|
+
} else {
|
|
419
|
+
child.kill('SIGTERM');
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
process.on('SIGINT', () => { stop(); process.exit(0); });
|
|
423
|
+
process.on('SIGTERM', () => { stop(); process.exit(0); });
|
|
424
|
+
if (platform() === 'win32') process.on('SIGHUP', () => { stop(); process.exit(0); });
|
|
425
|
+
|
|
426
|
+
console.log(`→ Subindo servidor local em ${url} ...`);
|
|
427
|
+
const health = await waitForHealth(porta, 120000);
|
|
428
|
+
if (!health) {
|
|
429
|
+
console.error(`✗ O servidor não respondeu em ${url} dentro do tempo limite.`);
|
|
430
|
+
console.error(' Veja os logs acima; rode "npx @danhachuel/leadmaster doctor" para diagnóstico.');
|
|
431
|
+
stop();
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
console.log(`✓ LeadMaster no ar em ${url} (dados: ${home})`);
|
|
435
|
+
openBrowser(url);
|
|
436
|
+
console.log('');
|
|
437
|
+
console.log('Pressione Ctrl+C para encerrar.');
|
|
438
|
+
})().catch((err) => {
|
|
439
|
+
console.error('✗ Erro inesperado ao iniciar:', err && err.message ? err.message : err);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function cmdDoctor(args) {
|
|
445
|
+
const home = resolveHome(args);
|
|
446
|
+
const portaInicial = Number.isFinite(args.port) && args.port > 0 ? args.port : UI_PORT;
|
|
447
|
+
console.log(`LeadMaster v${VERSION} — diagnóstico`);
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(`Node ............... ${process.version}`);
|
|
450
|
+
console.log(`Pasta de dados ..... ${home} ${existsSync(path.join(home, 'storage')) ? '(OK)' : '(ainda não criada — rode install)'}`);
|
|
451
|
+
|
|
452
|
+
const python = detectPython();
|
|
453
|
+
if (python) {
|
|
454
|
+
const v = pythonRun(python, ['--version'], { stdio: 'pipe' }).stdout.trim();
|
|
455
|
+
console.log(`Python ............. ${v} (${python.cmd})`);
|
|
456
|
+
console.log(`Dependências ....... ${pythonDepsOk(python) ? 'OK (fastapi, uvicorn, httpx, pydantic)' : 'AUSENTES — rode install'}`);
|
|
457
|
+
} else {
|
|
458
|
+
console.log('Python ............. NÃO ENCONTRADO no PATH');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const ativaEm = await procurarInstanciaAtiva(portaInicial);
|
|
462
|
+
console.log(`Servidor UI ........ ${ativaEm ? `rodando em http://127.0.0.1:${ativaEm}` : `parado (porta inicial ${portaInicial})`}`);
|
|
463
|
+
const hasDist = existsSync(path.join(PKG_ROOT, 'frontend', 'dist', 'index.html'));
|
|
464
|
+
console.log(`Interface (dist) ... ${hasDist ? 'presente' : 'ausente (modo dev ou rebuild necessário)'}`);
|
|
465
|
+
console.log('');
|
|
466
|
+
console.log('Diagnóstico concluído.');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function cmdHelp() {
|
|
470
|
+
console.log(`LeadMaster v${VERSION} — SaaS local de prospecção e automação de conteúdo`);
|
|
471
|
+
console.log('');
|
|
472
|
+
console.log('Uso: npx --yes @danhachuel/leadmaster@<versão> [comando] [flags]');
|
|
473
|
+
console.log('');
|
|
474
|
+
console.log('Comandos:');
|
|
475
|
+
console.log(' install Prepara a pasta de dados e instala dependências Python.');
|
|
476
|
+
console.log(' start Sobe o servidor local e abre o navegador (padrão).');
|
|
477
|
+
console.log(' doctor Diagnóstico do ambiente.');
|
|
478
|
+
console.log(' version Mostra a versão.');
|
|
479
|
+
console.log(' help Esta ajuda.');
|
|
480
|
+
console.log('');
|
|
481
|
+
console.log('Flags:');
|
|
482
|
+
console.log(' --home=<caminho> Pasta de dados (padrão: Documents\\REPOSITORIO\\LeadMaster).');
|
|
483
|
+
console.log(' --port=<numero> Porta inicial da UI (padrão: 5055; se ocupada, tenta 5056, 5057, ...).');
|
|
484
|
+
console.log(' --skip-python-deps Pula instalação de dependências Python (CI).');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// Main
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
const VERSION = (() => {
|
|
492
|
+
try {
|
|
493
|
+
return JSON.parse(readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
494
|
+
} catch {
|
|
495
|
+
return 'desconhecida';
|
|
496
|
+
}
|
|
497
|
+
})();
|
|
498
|
+
|
|
499
|
+
const rawArgs = process.argv.slice(2);
|
|
500
|
+
const args = parseArgs(rawArgs);
|
|
501
|
+
const command = args._[0] || 'start';
|
|
502
|
+
|
|
503
|
+
switch (command) {
|
|
504
|
+
case 'install': cmdInstall(args); break;
|
|
505
|
+
case 'start': cmdStart(args); break;
|
|
506
|
+
case 'doctor': cmdDoctor(args); break;
|
|
507
|
+
case 'version': console.log(VERSION); break;
|
|
508
|
+
case 'help': case '--help': case '-h': cmdHelp(); break;
|
|
509
|
+
default:
|
|
510
|
+
console.error(`Comando desconhecido: "${command}". Rode "npx @danhachuel/leadmaster help".`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|