pipely-ai 1.0.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.
Files changed (2) hide show
  1. package/index.js +915 -0
  2. package/package.json +29 -0
package/index.js ADDED
@@ -0,0 +1,915 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from "node:readline/promises";
4
+ import { createServer as createNetServer } from "node:net";
5
+ import { randomBytes } from "node:crypto";
6
+ import { execSync } from "node:child_process";
7
+ import { writeFileSync, readFileSync, existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { platform, release, arch } from "node:os";
10
+ import http from "node:http";
11
+
12
+ // ── ANSI Colors ──────────────────────────────────────
13
+
14
+ const c = {
15
+ reset: "\x1b[0m",
16
+ bold: "\x1b[1m",
17
+ dim: "\x1b[2m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ red: "\x1b[31m",
21
+ cyan: "\x1b[36m",
22
+ magenta: "\x1b[35m",
23
+ white: "\x1b[37m",
24
+ };
25
+
26
+ // ── Helpers ──────────────────────────────────────────
27
+
28
+ function sleep(ms) {
29
+ return new Promise((r) => setTimeout(r, ms));
30
+ }
31
+
32
+ function detectOS() {
33
+ const p = platform();
34
+ const r = release();
35
+ const a = arch();
36
+ const labels = {
37
+ win32: `Windows (${r})`,
38
+ darwin: `macOS (${r})`,
39
+ linux: `Linux (${r})`,
40
+ freebsd: `FreeBSD (${r})`,
41
+ };
42
+ return { platform: p, label: labels[p] || `${p} (${r})`, arch: a };
43
+ }
44
+
45
+ function checkDocker() {
46
+ try {
47
+ const version = execSync("docker --version", {
48
+ encoding: "utf-8",
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ }).trim();
51
+ const match = version.match(/Docker version ([^\s,]+)/);
52
+ return { ok: true, version: match ? match[1] : version };
53
+ } catch {
54
+ return { ok: false, version: null };
55
+ }
56
+ }
57
+
58
+ function getComposeCmd() {
59
+ try {
60
+ execSync("docker compose version", { stdio: "pipe" });
61
+ return "docker compose";
62
+ } catch {
63
+ try {
64
+ execSync("docker-compose version", { stdio: "pipe" });
65
+ return "docker-compose";
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+
72
+ function isPortFree(port) {
73
+ return new Promise((resolve) => {
74
+ const server = createNetServer();
75
+ server.once("error", () => resolve(false));
76
+ server.once("listening", () => {
77
+ server.close(() => {
78
+ try {
79
+ const out = execSync("docker ps --format '{{.Ports}}' 2>/dev/null", {
80
+ encoding: "utf-8",
81
+ stdio: ["pipe", "pipe", "pipe"],
82
+ });
83
+ resolve(!out.includes(`:${port}->`));
84
+ } catch {
85
+ resolve(true);
86
+ }
87
+ });
88
+ });
89
+ server.listen(port, "0.0.0.0");
90
+ });
91
+ }
92
+
93
+ function generateKey(length = 32) {
94
+ return randomBytes(Math.ceil(length / 2))
95
+ .toString("hex")
96
+ .slice(0, length);
97
+ }
98
+
99
+ function httpGet(url) {
100
+ return new Promise((resolve) => {
101
+ http
102
+ .get(url, (res) => resolve(res.statusCode === 200))
103
+ .on("error", () => resolve(false));
104
+ });
105
+ }
106
+
107
+ function commandExists(cmd) {
108
+ try {
109
+ execSync(`command -v ${cmd}`, { stdio: "pipe" });
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ function getServerIP() {
117
+ try {
118
+ return execSync(
119
+ "hostname -I 2>/dev/null || curl -s ifconfig.me 2>/dev/null",
120
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
121
+ )
122
+ .trim()
123
+ .split(/\s+/)[0] || null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function isRoot() {
130
+ try {
131
+ execSync("test $(id -u) -eq 0", { stdio: "pipe" });
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function findProjectDir() {
139
+ // Look for docker-compose.yml with pipely containers
140
+ const candidates = [process.cwd(), join(process.env.HOME || "/root", "pipely")];
141
+ for (const dir of candidates) {
142
+ const composePath = join(dir, "docker-compose.yml");
143
+ if (existsSync(composePath)) {
144
+ try {
145
+ const content = readFileSync(composePath, "utf-8");
146
+ if (content.includes("pipely")) return dir;
147
+ } catch {}
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+
153
+ function readEnvFile(dir) {
154
+ try {
155
+ const content = readFileSync(join(dir, ".env"), "utf-8");
156
+ const env = {};
157
+ for (const line of content.split("\n")) {
158
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
159
+ if (match) env[match[1]] = match[2];
160
+ }
161
+ return env;
162
+ } catch {
163
+ return {};
164
+ }
165
+ }
166
+
167
+ function requireProject() {
168
+ const dir = findProjectDir();
169
+ if (!dir) {
170
+ console.log(`\n ${c.red}✗ Projeto Pipely nao encontrado${c.reset}`);
171
+ console.log(` Execute na pasta onde rodou ${c.cyan}npx pipely-ai${c.reset} ou instale primeiro.\n`);
172
+ process.exit(1);
173
+ }
174
+ const composeCmd = getComposeCmd();
175
+ if (!composeCmd) {
176
+ console.log(`\n ${c.red}✗ Docker Compose nao encontrado${c.reset}\n`);
177
+ process.exit(1);
178
+ }
179
+ return { dir, composeCmd };
180
+ }
181
+
182
+ // ── Port prompt ─────────────────────────────────────
183
+
184
+ async function askPort(rl, label, defaultPort, takenPorts) {
185
+ while (true) {
186
+ const raw = await rl.question(` ${label} ${c.dim}[${defaultPort}]${c.reset}: `);
187
+ const port = parseInt(raw.trim() || String(defaultPort), 10);
188
+ if (isNaN(port) || port < 1 || port > 65535) {
189
+ console.log(` ${c.red}✗ Porta invalida (1-65535)${c.reset}\n`);
190
+ continue;
191
+ }
192
+ if (takenPorts.includes(port)) {
193
+ console.log(` ${c.red}✗ Porta ${port} ja escolhida para outro servico${c.reset}\n`);
194
+ continue;
195
+ }
196
+ process.stdout.write(` ${c.dim}→ Testando porta ${port}...${c.reset} `);
197
+ if (await isPortFree(port)) {
198
+ console.log(`${c.green}✓ Livre${c.reset}\n`);
199
+ return port;
200
+ }
201
+ console.log(`${c.red}✗ Em uso!${c.reset}\n`);
202
+ }
203
+ }
204
+
205
+ async function askYesNo(rl, question, defaultNo = true) {
206
+ const hint = defaultNo ? "s/N" : "S/n";
207
+ const raw = await rl.question(` ${question} ${c.dim}[${hint}]${c.reset}: `);
208
+ const answer = raw.trim().toLowerCase();
209
+ if (defaultNo) return answer === "s" || answer === "y";
210
+ return answer !== "n";
211
+ }
212
+
213
+ // ── Docker Compose template ─────────────────────────
214
+
215
+ function generateCompose(ports, domain) {
216
+ const frontendUrl = domain
217
+ ? `https://${domain}`
218
+ : `http://localhost:\${FRONTEND_PORT:-${ports.frontend}}`;
219
+
220
+ return `# Pipely AI — gerado por npx pipely-ai
221
+ # Docs: https://github.com/Pedro-Furtado/pipely-ai
222
+
223
+ services:
224
+ db:
225
+ image: postgres:17-alpine
226
+ container_name: pipely-db
227
+ restart: unless-stopped
228
+ environment:
229
+ POSTGRES_USER: pipely
230
+ POSTGRES_PASSWORD: \${DB_PASSWORD}
231
+ POSTGRES_DB: pipely_ai
232
+ ports:
233
+ - "\${DB_PORT:-${ports.db}}:5432"
234
+ volumes:
235
+ - pgdata:/var/lib/postgresql/data
236
+ - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
237
+ healthcheck:
238
+ test: ["CMD-SHELL", "pg_isready -U pipely -d pipely_ai"]
239
+ interval: 5s
240
+ timeout: 3s
241
+ retries: 10
242
+
243
+ evolution:
244
+ image: evoapicloud/evolution-go:latest
245
+ container_name: pipely-evolution
246
+ restart: unless-stopped
247
+ environment:
248
+ - GLOBAL_API_KEY=\${EVOLUTION_API_KEY}
249
+ - CLIENT_NAME=pipely
250
+ - SERVER_PORT=8080
251
+ - DATABASE_SAVE_MESSAGES=true
252
+ - POSTGRES_AUTH_DB=postgresql://pipely:\${DB_PASSWORD}@db:5432/evolution_go?sslmode=disable
253
+ - POSTGRES_USERS_DB=postgresql://pipely:\${DB_PASSWORD}@db:5432/evolution_go?sslmode=disable
254
+ - LOGTYPE=text
255
+ - WA_DEBUG=false
256
+ ports:
257
+ - "\${EVOLUTION_PORT:-${ports.evolution}}:8080"
258
+ depends_on:
259
+ db:
260
+ condition: service_healthy
261
+
262
+ app:
263
+ image: ghcr.io/pedro-furtado/pipely-ai:latest
264
+ container_name: pipely-app
265
+ restart: unless-stopped
266
+ environment:
267
+ - DATABASE_URL=postgresql://pipely:\${DB_PASSWORD}@db:5432/pipely_ai
268
+ - JWT_SECRET=\${JWT_SECRET}
269
+ - FRONTEND_URL=${frontendUrl}
270
+ - BACKEND_URL=http://127.0.0.1:3333
271
+ - POLL_INTERVAL_MS=\${POLL_INTERVAL_MS:-60000}
272
+ - EVOLUTION_SERVER_URL=http://evolution:8080
273
+ - EVOLUTION_API_KEY=\${EVOLUTION_API_KEY}
274
+ - NODE_ENV=production
275
+ ports:
276
+ - "\${FRONTEND_PORT:-${ports.frontend}}:80"
277
+ - "\${BACKEND_PORT:-${ports.backend}}:3333"
278
+ - "\${AGENT_PORT:-${ports.agent}}:3335"
279
+ depends_on:
280
+ db:
281
+ condition: service_healthy
282
+ evolution:
283
+ condition: service_started
284
+
285
+ volumes:
286
+ pgdata:
287
+ `;
288
+ }
289
+
290
+ // ── .env template ───────────────────────────────────
291
+
292
+ function generateEnv(ports, keys, domain) {
293
+ const now = new Date().toISOString().split("T")[0];
294
+ let env = `# Pipely AI — gerado por npx pipely-ai em ${now}
295
+ # Docs: https://github.com/Pedro-Furtado/pipely-ai
296
+
297
+ # ── Portas ────────────────────────────────
298
+ FRONTEND_PORT=${ports.frontend}
299
+ BACKEND_PORT=${ports.backend}
300
+ AGENT_PORT=${ports.agent}
301
+ EVOLUTION_PORT=${ports.evolution}
302
+ DB_PORT=${ports.db}
303
+
304
+ # ── Seguranca (auto-gerado — NAO compartilhe) ──
305
+ DB_PASSWORD=${keys.dbPassword}
306
+ JWT_SECRET=${keys.jwtSecret}
307
+ EVOLUTION_API_KEY=${keys.evolutionApiKey}
308
+
309
+ # ── App ───────────────────────────────────
310
+ POLL_INTERVAL_MS=60000
311
+ `;
312
+ if (domain) {
313
+ env += `\n# ── Dominio ───────────────────────────────\nAPP_DOMAIN=${domain}\n`;
314
+ }
315
+ return env;
316
+ }
317
+
318
+ const INIT_DB_SH = `#!/bin/bash
319
+ set -e
320
+ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
321
+ SELECT 'CREATE DATABASE evolution_go' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'evolution_go')\\gexec
322
+ EOSQL
323
+ `;
324
+
325
+ // ── Wait for health ─────────────────────────────────
326
+
327
+ async function waitForHealth(port, maxWaitMs = 120000) {
328
+ const start = Date.now();
329
+ let dots = 0;
330
+ while (Date.now() - start < maxWaitMs) {
331
+ if (await httpGet(`http://localhost:${port}/health`)) return true;
332
+ await sleep(2000);
333
+ if (++dots % 3 === 0) process.stdout.write(".");
334
+ }
335
+ return false;
336
+ }
337
+
338
+ // ── Extract setup key from logs ─────────────────────
339
+
340
+ function getSetupKey(composeCmd, cwd) {
341
+ try {
342
+ const logs = execSync(`${composeCmd} logs app 2>&1`, {
343
+ cwd,
344
+ encoding: "utf-8",
345
+ maxBuffer: 10 * 1024 * 1024,
346
+ });
347
+ const match = logs.match(
348
+ /SETUP KEY[\s\S]*?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/
349
+ );
350
+ return match ? match[1] : null;
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+
356
+ // ── Domain + SSL ────────────────────────────────────
357
+
358
+ function setupDomainSSL(domain, port) {
359
+ if (!commandExists("nginx")) {
360
+ process.stdout.write(` Instalando Nginx... `);
361
+ try {
362
+ execSync("apt-get update -qq && apt-get install -y -qq nginx > /dev/null 2>&1", { stdio: "pipe" });
363
+ execSync("systemctl enable nginx && systemctl start nginx", { stdio: "pipe" });
364
+ console.log(`${c.green}✓${c.reset}`);
365
+ } catch {
366
+ console.log(`${c.red}✗${c.reset}`);
367
+ console.log(` ${c.red}Erro ao instalar Nginx${c.reset}\n`);
368
+ return false;
369
+ }
370
+ } else {
371
+ console.log(` ${c.green}✓${c.reset} Nginx instalado`);
372
+ }
373
+
374
+ if (!commandExists("certbot")) {
375
+ process.stdout.write(` Instalando Certbot... `);
376
+ try {
377
+ execSync("apt-get update -qq && apt-get install -y -qq certbot python3-certbot-nginx > /dev/null 2>&1", { stdio: "pipe" });
378
+ console.log(`${c.green}✓${c.reset}`);
379
+ } catch {
380
+ console.log(`${c.red}✗${c.reset}`);
381
+ console.log(` ${c.red}Erro ao instalar Certbot${c.reset}\n`);
382
+ return false;
383
+ }
384
+ } else {
385
+ console.log(` ${c.green}✓${c.reset} Certbot instalado`);
386
+ }
387
+
388
+ try { execSync("rm -f /etc/nginx/sites-enabled/default", { stdio: "pipe" }); } catch {}
389
+
390
+ process.stdout.write(` Configurando Nginx... `);
391
+ const nginxConfig = `server {
392
+ listen 80;
393
+ server_name ${domain};
394
+ large_client_header_buffers 4 32k;
395
+ location / {
396
+ proxy_pass http://127.0.0.1:${port};
397
+ proxy_http_version 1.1;
398
+ proxy_set_header Host $host;
399
+ proxy_set_header X-Real-IP $remote_addr;
400
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
401
+ proxy_set_header X-Forwarded-Proto $scheme;
402
+ proxy_set_header Upgrade $http_upgrade;
403
+ proxy_set_header Connection "upgrade";
404
+ proxy_buffering off;
405
+ proxy_read_timeout 86400;
406
+ }
407
+ }
408
+ `;
409
+ try {
410
+ writeFileSync("/etc/nginx/sites-available/pipely", nginxConfig);
411
+ execSync("ln -sf /etc/nginx/sites-available/pipely /etc/nginx/sites-enabled/", { stdio: "pipe" });
412
+ execSync("nginx -t", { stdio: "pipe" });
413
+ execSync("systemctl reload nginx", { stdio: "pipe" });
414
+ console.log(`${c.green}✓${c.reset}`);
415
+ } catch {
416
+ console.log(`${c.red}✗${c.reset}`);
417
+ console.log(` ${c.yellow}⚠ Nginx falhou (porta 80 pode estar em uso por outro proxy)${c.reset}`);
418
+ console.log(` ${c.dim}Se usa Caddy/Traefik, configure o proxy para ${domain} → localhost:${port}${c.reset}`);
419
+ return false;
420
+ }
421
+
422
+ process.stdout.write(` Configurando SSL... `);
423
+ try {
424
+ execSync(`certbot --nginx -d ${domain} --non-interactive --agree-tos --register-unsafely-without-email 2>&1`, { stdio: "pipe" });
425
+ console.log(`${c.green}✓${c.reset}`);
426
+ return true;
427
+ } catch {
428
+ console.log(`${c.yellow}✗${c.reset}`);
429
+ console.log(` ${c.yellow}⚠ SSL falhou. Verifique DNS A record.${c.reset}`);
430
+ console.log(` ${c.dim}Tente depois: certbot --nginx -d ${domain}${c.reset}`);
431
+ return false;
432
+ }
433
+ }
434
+
435
+ // ── Docker install help ─────────────────────────────
436
+
437
+ function printDockerHelp(os) {
438
+ console.log(`\n ${c.red}✗ Docker nao encontrado${c.reset}\n`);
439
+ if (os.platform === "win32") {
440
+ console.log(` Windows: https://docs.docker.com/desktop/install/windows-install/\n`);
441
+ } else if (os.platform === "darwin") {
442
+ console.log(` macOS: https://docs.docker.com/desktop/install/mac-install/\n`);
443
+ } else {
444
+ console.log(` Linux: curl -fsSL https://get.docker.com | sh\n`);
445
+ }
446
+ }
447
+
448
+ // ── Banner ──────────────────────────────────────────
449
+
450
+ function printBanner() {
451
+ console.log("");
452
+ console.log(` ${c.magenta}${c.bold}╔═══════════════════════════════════════════╗${c.reset}`);
453
+ console.log(` ${c.magenta}${c.bold}║ PIPELY AI ║${c.reset}`);
454
+ console.log(` ${c.magenta}${c.bold}║ Automacao de tarefas + WhatsApp ║${c.reset}`);
455
+ console.log(` ${c.magenta}${c.bold}╚═══════════════════════════════════════════╝${c.reset}`);
456
+ console.log("");
457
+ }
458
+
459
+ // ── Summary (after install) ─────────────────────────
460
+
461
+ function printInstallSummary(ports, keys, setupKey, domain, sslOk) {
462
+ const line = "═".repeat(56);
463
+ const baseUrl = domain && sslOk ? `https://${domain}` : domain ? `http://${domain}` : `http://localhost:${ports.frontend}`;
464
+ const ip = getServerIP();
465
+ const evolutionUrl = domain ? `http://${ip || "SEU_IP"}:${ports.evolution}` : `http://localhost:${ports.evolution}`;
466
+
467
+ console.log("");
468
+ console.log(` ${c.green}${line}${c.reset}`);
469
+ console.log(` ${c.green}${c.bold} PIPELY AI — PRONTO!${c.reset}`);
470
+ console.log(` ${c.green}${line}${c.reset}`);
471
+ console.log("");
472
+ console.log(` ${c.bold}Endpoints:${c.reset}`);
473
+ console.log(` Frontend: ${c.cyan}${baseUrl}${c.reset}`);
474
+ console.log(` Backend API: ${c.cyan}${baseUrl}/api${c.reset}`);
475
+ if (!domain) {
476
+ console.log(` Backend direto: ${c.cyan}http://localhost:${ports.backend}${c.reset}`);
477
+ console.log(` Agent Webhook: ${c.cyan}http://localhost:${ports.agent}/webhook${c.reset}`);
478
+ }
479
+ console.log(` Evolution Go: ${c.cyan}${evolutionUrl}${c.reset}`);
480
+ console.log(` Evolution Mgr: ${c.cyan}${evolutionUrl}/manager${c.reset}`);
481
+ console.log("");
482
+ console.log(` ${c.bold}Chaves:${c.reset}`);
483
+ console.log(` Evolution Key: ${c.yellow}${keys.evolutionApiKey}${c.reset}`);
484
+ if (setupKey) {
485
+ console.log(` Setup Key: ${c.yellow}${setupKey}${c.reset}`);
486
+ }
487
+ console.log("");
488
+ console.log(` ${c.bold}Proximo passo:${c.reset}`);
489
+ console.log(` 1. Acesse ${c.cyan}${baseUrl}/setup${c.reset}`);
490
+ if (setupKey) {
491
+ console.log(` 2. Use a Setup Key acima para criar sua conta`);
492
+ console.log(` 3. Configure WhatsApp e OpenAI nas paginas do app`);
493
+ } else {
494
+ console.log(` 2. Veja a Setup Key: ${c.dim}npx pipely-ai keys${c.reset}`);
495
+ console.log(` 3. Configure WhatsApp e OpenAI nas paginas do app`);
496
+ }
497
+ console.log("");
498
+ console.log(` ${c.bold}Comandos:${c.reset}`);
499
+ console.log(` npx pipely-ai status ${c.dim}# Ver status e URLs${c.reset}`);
500
+ console.log(` npx pipely-ai keys ${c.dim}# Ver chaves${c.reset}`);
501
+ console.log(` npx pipely-ai logs ${c.dim}# Ver logs${c.reset}`);
502
+ console.log(` npx pipely-ai stop ${c.dim}# Parar${c.reset}`);
503
+ console.log(` npx pipely-ai start ${c.dim}# Iniciar${c.reset}`);
504
+ console.log(` npx pipely-ai update ${c.dim}# Atualizar${c.reset}`);
505
+ console.log("");
506
+ console.log(` ${c.green}${line}${c.reset}`);
507
+ console.log("");
508
+ }
509
+
510
+ // ══════════════════════════════════════════════════════
511
+ // ── COMMANDS ─────────────────────────────────────────
512
+ // ══════════════════════════════════════════════════════
513
+
514
+ // ── status ──────────────────────────────────────────
515
+
516
+ function cmdStatus() {
517
+ const { dir, composeCmd } = requireProject();
518
+ const env = readEnvFile(dir);
519
+ const domain = env.APP_DOMAIN || null;
520
+ const ip = getServerIP();
521
+ const frontendPort = env.FRONTEND_PORT || "3000";
522
+ const evolutionPort = env.EVOLUTION_PORT || "8080";
523
+ const baseUrl = domain ? `https://${domain}` : `http://${ip || "localhost"}:${frontendPort}`;
524
+ const evolutionUrl = `http://${ip || "localhost"}:${evolutionPort}`;
525
+
526
+ console.log("");
527
+ console.log(` ${c.bold}PIPELY AI — Status${c.reset}\n`);
528
+
529
+ // Container status
530
+ try {
531
+ const ps = execSync(`${composeCmd} ps --format "table {{.Name}}\t{{.State}}\t{{.Ports}}"`, {
532
+ cwd: dir,
533
+ encoding: "utf-8",
534
+ });
535
+ console.log(` ${c.bold}Containers:${c.reset}`);
536
+ for (const line of ps.trim().split("\n")) {
537
+ const state = line.includes("running") ? c.green : line.includes("exited") ? c.red : c.dim;
538
+ console.log(` ${state}${line}${c.reset}`);
539
+ }
540
+ } catch {
541
+ console.log(` ${c.red}Nenhum container rodando${c.reset}`);
542
+ }
543
+
544
+ console.log("");
545
+ console.log(` ${c.bold}Endpoints:${c.reset}`);
546
+ console.log(` Frontend: ${c.cyan}${baseUrl}${c.reset}`);
547
+ console.log(` Backend API: ${c.cyan}${baseUrl}/api${c.reset}`);
548
+ console.log(` Evolution Go: ${c.cyan}${evolutionUrl}${c.reset}`);
549
+ console.log(` Evolution Mgr: ${c.cyan}${evolutionUrl}/manager${c.reset}`);
550
+
551
+ if (domain) {
552
+ console.log(`\n ${c.bold}Dominio:${c.reset} ${c.cyan}${domain}${c.reset}`);
553
+ }
554
+
555
+ console.log(`\n ${c.bold}Diretorio:${c.reset} ${c.dim}${dir}${c.reset}`);
556
+ console.log("");
557
+ }
558
+
559
+ // ── keys ────────────────────────────────────────────
560
+
561
+ function cmdKeys() {
562
+ const { dir, composeCmd } = requireProject();
563
+ const env = readEnvFile(dir);
564
+
565
+ console.log("");
566
+ console.log(` ${c.bold}PIPELY AI — Chaves${c.reset}\n`);
567
+
568
+ if (env.EVOLUTION_API_KEY) {
569
+ console.log(` Evolution Key: ${c.yellow}${env.EVOLUTION_API_KEY}${c.reset}`);
570
+ }
571
+ if (env.JWT_SECRET) {
572
+ console.log(` JWT Secret: ${c.yellow}${env.JWT_SECRET.slice(0, 16)}...${c.reset}`);
573
+ }
574
+
575
+ // Setup key from logs
576
+ const setupKey = getSetupKey(composeCmd, dir);
577
+ if (setupKey) {
578
+ console.log(` Setup Key: ${c.yellow}${setupKey}${c.reset}`);
579
+ } else {
580
+ console.log(` Setup Key: ${c.dim}(nao encontrada nos logs — conta ja pode ter sido criada)${c.reset}`);
581
+ }
582
+
583
+ console.log("");
584
+ }
585
+
586
+ // ── logs ────────────────────────────────────────────
587
+
588
+ function cmdLogs(service) {
589
+ const { dir, composeCmd } = requireProject();
590
+ const target = service || "app";
591
+ try {
592
+ execSync(`${composeCmd} logs -f --tail 100 ${target}`, {
593
+ cwd: dir,
594
+ stdio: "inherit",
595
+ });
596
+ } catch {
597
+ // User pressed Ctrl+C
598
+ }
599
+ }
600
+
601
+ // ── stop ────────────────────────────────────────────
602
+
603
+ function cmdStop() {
604
+ const { dir, composeCmd } = requireProject();
605
+ console.log(`\n Parando containers...`);
606
+ try {
607
+ execSync(`${composeCmd} down`, { cwd: dir, stdio: "inherit" });
608
+ console.log(`\n ${c.green}✓${c.reset} Containers parados\n`);
609
+ } catch {
610
+ console.log(`\n ${c.red}✗ Erro ao parar containers${c.reset}\n`);
611
+ }
612
+ }
613
+
614
+ // ── start ───────────────────────────────────────────
615
+
616
+ function cmdStart() {
617
+ const { dir, composeCmd } = requireProject();
618
+ console.log(`\n Iniciando containers...`);
619
+ try {
620
+ execSync(`${composeCmd} up -d`, { cwd: dir, stdio: "inherit" });
621
+ console.log(`\n ${c.green}✓${c.reset} Containers iniciados\n`);
622
+ } catch {
623
+ console.log(`\n ${c.red}✗ Erro ao iniciar containers${c.reset}\n`);
624
+ }
625
+ }
626
+
627
+ // ── restart ─────────────────────────────────────────
628
+
629
+ function cmdRestart() {
630
+ const { dir, composeCmd } = requireProject();
631
+ console.log(`\n Reiniciando containers...`);
632
+ try {
633
+ execSync(`${composeCmd} restart`, { cwd: dir, stdio: "inherit" });
634
+ console.log(`\n ${c.green}✓${c.reset} Containers reiniciados\n`);
635
+ } catch {
636
+ console.log(`\n ${c.red}✗ Erro ao reiniciar containers${c.reset}\n`);
637
+ }
638
+ }
639
+
640
+ // ── update ──────────────────────────────────────────
641
+
642
+ function cmdUpdate() {
643
+ const { dir, composeCmd } = requireProject();
644
+ console.log(`\n ${c.bold}Atualizando Pipely AI...${c.reset}\n`);
645
+
646
+ process.stdout.write(` Baixando imagem nova... `);
647
+ try {
648
+ execSync(`${composeCmd} pull app`, { cwd: dir, stdio: "pipe" });
649
+ console.log(`${c.green}✓${c.reset}`);
650
+ } catch {
651
+ console.log(`${c.red}✗${c.reset}`);
652
+ console.log(` ${c.red}Erro ao baixar imagem${c.reset}\n`);
653
+ return;
654
+ }
655
+
656
+ process.stdout.write(` Recriando container... `);
657
+ try {
658
+ execSync(`${composeCmd} up -d --no-deps app`, { cwd: dir, stdio: "pipe" });
659
+ console.log(`${c.green}✓${c.reset}`);
660
+ } catch {
661
+ console.log(`${c.red}✗${c.reset}`);
662
+ console.log(` ${c.red}Erro ao recriar container${c.reset}\n`);
663
+ return;
664
+ }
665
+
666
+ console.log(`\n ${c.green}✓${c.reset} Atualizado com sucesso!\n`);
667
+ }
668
+
669
+ // ── help ────────────────────────────────────────────
670
+
671
+ function cmdHelp() {
672
+ printBanner();
673
+ console.log(` ${c.bold}Comandos:${c.reset}\n`);
674
+ console.log(` ${c.cyan}npx pipely-ai${c.reset} Instalar Pipely AI`);
675
+ console.log(` ${c.cyan}npx pipely-ai status${c.reset} Ver status, endpoints e dominio`);
676
+ console.log(` ${c.cyan}npx pipely-ai keys${c.reset} Ver chaves (Evolution, Setup Key)`);
677
+ console.log(` ${c.cyan}npx pipely-ai logs${c.reset} Ver logs do app (Ctrl+C para sair)`);
678
+ console.log(` ${c.cyan}npx pipely-ai logs db${c.reset} Ver logs do banco de dados`);
679
+ console.log(` ${c.cyan}npx pipely-ai stop${c.reset} Parar todos os containers`);
680
+ console.log(` ${c.cyan}npx pipely-ai start${c.reset} Iniciar containers`);
681
+ console.log(` ${c.cyan}npx pipely-ai restart${c.reset} Reiniciar containers`);
682
+ console.log(` ${c.cyan}npx pipely-ai update${c.reset} Atualizar para ultima versao`);
683
+ console.log(` ${c.cyan}npx pipely-ai help${c.reset} Mostrar esta ajuda`);
684
+ console.log("");
685
+ }
686
+
687
+ // ══════════════════════════════════════════════════════
688
+ // ── INSTALL (main) ───────────────────────────────────
689
+ // ══════════════════════════════════════════════════════
690
+
691
+ async function install() {
692
+ printBanner();
693
+
694
+ const os = detectOS();
695
+ console.log(` Sistema: ${c.bold}${os.label}${c.reset} (${os.arch})\n`);
696
+
697
+ process.stdout.write(` Verificando Docker... `);
698
+ const docker = checkDocker();
699
+ if (!docker.ok) {
700
+ printDockerHelp(os);
701
+ process.exit(1);
702
+ }
703
+ console.log(`${c.green}✓${c.reset} v${docker.version}`);
704
+
705
+ const composeCmd = getComposeCmd();
706
+ if (!composeCmd) {
707
+ console.log(`\n ${c.red}✗ Docker Compose nao encontrado${c.reset}\n`);
708
+ process.exit(1);
709
+ }
710
+
711
+ try {
712
+ execSync("docker info", { stdio: "pipe" });
713
+ } catch {
714
+ console.log(`\n ${c.red}✗ Docker nao esta rodando${c.reset}`);
715
+ if (os.platform === "win32" || os.platform === "darwin") {
716
+ console.log(` Abra o Docker Desktop e tente novamente.\n`);
717
+ } else {
718
+ console.log(` Execute: sudo systemctl start docker\n`);
719
+ }
720
+ process.exit(1);
721
+ }
722
+
723
+ console.log("");
724
+
725
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
726
+ let done = false;
727
+ rl.on("close", () => {
728
+ if (!done) {
729
+ console.log(`\n\n ${c.yellow}Instalacao cancelada.${c.reset}\n`);
730
+ process.exit(0);
731
+ }
732
+ });
733
+
734
+ // ── Ports ──
735
+ console.log(` ${c.magenta}── Configuracao de Portas ──────────────────${c.reset}\n`);
736
+ console.log(` Pressione ${c.bold}Enter${c.reset} para usar o valor recomendado.\n`);
737
+
738
+ const takenPorts = [];
739
+ const frontendPort = await askPort(rl, "Aplicacao (Frontend + API)", 3000, takenPorts);
740
+ takenPorts.push(frontendPort);
741
+ const backendPort = await askPort(rl, "Backend API (acesso direto)", 3333, takenPorts);
742
+ takenPorts.push(backendPort);
743
+ const agentPort = await askPort(rl, "Agent Webhook", 3335, takenPorts);
744
+ takenPorts.push(agentPort);
745
+ const evolutionPort = await askPort(rl, "Evolution Go (WhatsApp)", 8080, takenPorts);
746
+ takenPorts.push(evolutionPort);
747
+ const dbPort = await askPort(rl, "PostgreSQL", 5433, takenPorts);
748
+ takenPorts.push(dbPort);
749
+
750
+ const ports = { frontend: frontendPort, backend: backendPort, agent: agentPort, evolution: evolutionPort, db: dbPort };
751
+
752
+ // ── Keys ──
753
+ console.log(` ${c.magenta}── Seguranca ──────────────────────────────${c.reset}\n`);
754
+ const keys = { dbPassword: generateKey(32), jwtSecret: generateKey(64), evolutionApiKey: generateKey(32) };
755
+ console.log(` ${c.green}✓${c.reset} DB_PASSWORD gerado`);
756
+ console.log(` ${c.green}✓${c.reset} JWT_SECRET gerado`);
757
+ console.log(` ${c.green}✓${c.reset} EVOLUTION_API_KEY gerado`);
758
+ console.log("");
759
+
760
+ // ── Domain (optional) ──
761
+ let domain = null;
762
+ let sslOk = false;
763
+ const isLinux = os.platform === "linux";
764
+ const hasRoot = isLinux && isRoot();
765
+
766
+ console.log(` ${c.magenta}── Dominio (opcional) ─────────────────────${c.reset}\n`);
767
+
768
+ if (!isLinux) {
769
+ console.log(` ${c.dim}Dominio + SSL disponivel apenas em servidores Linux (VPS)${c.reset}\n`);
770
+ } else if (!hasRoot) {
771
+ console.log(` ${c.dim}Dominio + SSL requer root. Execute com sudo para habilitar.${c.reset}\n`);
772
+ } else {
773
+ const wantsDomain = await askYesNo(rl, "Configurar dominio + SSL?");
774
+ if (wantsDomain) {
775
+ const ip = getServerIP();
776
+ if (ip) {
777
+ console.log("");
778
+ console.log(` IP do servidor: ${c.cyan}${ip}${c.reset}`);
779
+ console.log(` ${c.dim}Aponte o DNS A record do dominio para este IP${c.reset}\n`);
780
+ }
781
+ const rawDomain = await rl.question(` Dominio (ex: pipely.seusite.com): `);
782
+ domain = rawDomain.trim();
783
+ if (!domain) {
784
+ console.log(` ${c.yellow}⚠ Dominio vazio — continuando sem SSL${c.reset}\n`);
785
+ domain = null;
786
+ } else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}$/.test(domain)) {
787
+ console.log(` ${c.yellow}⚠ Formato invalido — continuando sem SSL${c.reset}\n`);
788
+ domain = null;
789
+ }
790
+ } else {
791
+ console.log("");
792
+ }
793
+ }
794
+
795
+ done = true;
796
+ rl.close();
797
+
798
+ // ── Generate Files ──
799
+ const targetDir = process.cwd();
800
+ console.log(` ${c.magenta}── Gerando configuracao ────────────────────${c.reset}\n`);
801
+
802
+ writeFileSync(join(targetDir, "docker-compose.yml"), generateCompose(ports, domain));
803
+ writeFileSync(join(targetDir, ".env"), generateEnv(ports, keys, domain));
804
+ writeFileSync(join(targetDir, "init-db.sh"), INIT_DB_SH);
805
+
806
+ if (os.platform !== "win32") {
807
+ try { execSync(`chmod +x "${join(targetDir, "init-db.sh")}"`, { stdio: "pipe" }); } catch {}
808
+ }
809
+
810
+ console.log(` ${c.green}✓${c.reset} docker-compose.yml`);
811
+ console.log(` ${c.green}✓${c.reset} .env`);
812
+ console.log(` ${c.green}✓${c.reset} init-db.sh`);
813
+
814
+ // ── Pull & Start ──
815
+ console.log(`\n ${c.magenta}── Iniciando ──────────────────────────────${c.reset}\n`);
816
+ console.log(` Baixando imagens Docker ${c.dim}(pode demorar na primeira vez)${c.reset}...\n`);
817
+
818
+ try {
819
+ execSync(`${composeCmd} pull`, { cwd: targetDir, stdio: "inherit" });
820
+ } catch {
821
+ console.log(`\n ${c.red}✗ Erro ao baixar imagens${c.reset}\n`);
822
+ process.exit(1);
823
+ }
824
+
825
+ console.log(`\n Iniciando containers...`);
826
+ try {
827
+ execSync(`${composeCmd} up -d`, { cwd: targetDir, stdio: "pipe" });
828
+ console.log(` ${c.green}✓${c.reset} Containers iniciados`);
829
+ } catch (err) {
830
+ const stderr = err.stderr?.toString() || "";
831
+ const portMatch = stderr.match(/Bind for .+:(\d+) failed: port is already allocated/);
832
+ if (portMatch) {
833
+ console.log(` ${c.red}✗ Porta ${portMatch[1]} esta ocupada${c.reset}`);
834
+ console.log(` ${c.dim}docker ps # para ver containers rodando${c.reset}`);
835
+ } else {
836
+ console.log(` ${c.red}✗ Erro ao iniciar containers${c.reset}`);
837
+ if (stderr) console.log(` ${c.dim}${stderr.trim()}${c.reset}`);
838
+ }
839
+ process.exit(1);
840
+ }
841
+
842
+ // ── Wait for Health ──
843
+ console.log(`\n Aguardando servicos ficarem prontos...`);
844
+ process.stdout.write(" ");
845
+ const healthy = await waitForHealth(ports.frontend, 120000);
846
+ if (healthy) {
847
+ console.log(`\n ${c.green}✓${c.reset} Aplicacao pronta`);
848
+ } else {
849
+ console.log(`\n ${c.yellow}⚠ Timeout — verifique: ${composeCmd} logs -f app${c.reset}`);
850
+ }
851
+
852
+ // ── Domain + SSL ──
853
+ if (domain && hasRoot) {
854
+ console.log("");
855
+ console.log(` ${c.magenta}── Configurando dominio + SSL ─────────────${c.reset}\n`);
856
+ sslOk = setupDomainSSL(domain, ports.frontend);
857
+ }
858
+
859
+ // ── Setup Key ──
860
+ let setupKey = null;
861
+ if (healthy) {
862
+ await sleep(3000);
863
+ setupKey = getSetupKey(composeCmd, targetDir);
864
+ if (!setupKey) {
865
+ for (let i = 0; i < 5; i++) {
866
+ await sleep(2000);
867
+ setupKey = getSetupKey(composeCmd, targetDir);
868
+ if (setupKey) break;
869
+ }
870
+ }
871
+ }
872
+
873
+ printInstallSummary(ports, keys, setupKey, domain, sslOk);
874
+ }
875
+
876
+ // ══════════════════════════════════════════════════════
877
+ // ── ROUTER ───────────────────────────────────────────
878
+ // ══════════════════════════════════════════════════════
879
+
880
+ const args = process.argv.slice(2);
881
+ const command = args[0];
882
+
883
+ switch (command) {
884
+ case "status":
885
+ cmdStatus();
886
+ break;
887
+ case "keys":
888
+ cmdKeys();
889
+ break;
890
+ case "logs":
891
+ cmdLogs(args[1]);
892
+ break;
893
+ case "stop":
894
+ cmdStop();
895
+ break;
896
+ case "start":
897
+ cmdStart();
898
+ break;
899
+ case "restart":
900
+ cmdRestart();
901
+ break;
902
+ case "update":
903
+ cmdUpdate();
904
+ break;
905
+ case "help":
906
+ case "--help":
907
+ case "-h":
908
+ cmdHelp();
909
+ break;
910
+ default:
911
+ install().catch((err) => {
912
+ console.error(`\n ${c.red}Erro: ${err.message}${c.reset}\n`);
913
+ process.exit(1);
914
+ });
915
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "pipely-ai",
3
+ "version": "1.0.0",
4
+ "description": "Pipely AI — Instale, gerencie e atualize com um unico comando",
5
+ "type": "module",
6
+ "bin": {
7
+ "pipely-ai": "index.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "index.js"
14
+ ],
15
+ "keywords": [
16
+ "pipely",
17
+ "task-management",
18
+ "whatsapp",
19
+ "automation",
20
+ "pipeline",
21
+ "ai-agent"
22
+ ],
23
+ "author": "Pedro Furtado",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Pedro-Furtado/pipely-ai.git"
28
+ }
29
+ }