pipely-ai 1.1.0 → 1.2.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 +340 -18
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -446,6 +446,274 @@ function printDockerHelp(os) {
446
446
  }
447
447
  }
448
448
 
449
+ // ── Local Mode ──────────────────────────────────────
450
+
451
+ const PIPELY_DIR = join(process.env.HOME || process.env.USERPROFILE || ".", ".pipely");
452
+ const BUNDLE_REPO = "Pedro-Furtado/pipely-ai";
453
+ const BUNDLE_NAME = "pipely-local.tar.gz";
454
+
455
+ async function getLatestReleaseUrl() {
456
+ return new Promise((resolve) => {
457
+ const url = `https://api.github.com/repos/${BUNDLE_REPO}/releases/latest`;
458
+ const https = require("node:https");
459
+ https.get(url, { headers: { "User-Agent": "pipely-cli" } }, (res) => {
460
+ let body = "";
461
+ res.on("data", (chunk) => (body += chunk));
462
+ res.on("end", () => {
463
+ try {
464
+ const release = JSON.parse(body);
465
+ const asset = (release.assets || []).find((a) => a.name === BUNDLE_NAME);
466
+ resolve(asset?.browser_download_url || null);
467
+ } catch {
468
+ resolve(null);
469
+ }
470
+ });
471
+ }).on("error", () => resolve(null));
472
+ });
473
+ }
474
+
475
+ async function downloadFile(url, dest) {
476
+ return new Promise((resolve, reject) => {
477
+ const https = require("node:https");
478
+ const fs = require("node:fs");
479
+
480
+ function follow(url) {
481
+ https.get(url, { headers: { "User-Agent": "pipely-cli" } }, (res) => {
482
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
483
+ follow(res.headers.location);
484
+ return;
485
+ }
486
+ if (res.statusCode !== 200) {
487
+ reject(new Error(`HTTP ${res.statusCode}`));
488
+ return;
489
+ }
490
+ const file = fs.createWriteStream(dest);
491
+ res.pipe(file);
492
+ file.on("finish", () => { file.close(); resolve(); });
493
+ }).on("error", reject);
494
+ }
495
+
496
+ follow(url);
497
+ });
498
+ }
499
+
500
+ function extractTarGz(file, dest) {
501
+ const { mkdirSync } = require("node:fs");
502
+ mkdirSync(dest, { recursive: true });
503
+
504
+ const os = detectOS();
505
+ if (os.platform === "win32") {
506
+ // Windows: use tar (available since Windows 10)
507
+ execSync(`tar -xzf "${file}" -C "${dest}"`, { stdio: "pipe" });
508
+ } else {
509
+ execSync(`tar -xzf "${file}" -C "${dest}"`, { stdio: "pipe" });
510
+ }
511
+ }
512
+
513
+ function getLocalEnvPath() {
514
+ return join(PIPELY_DIR, ".env");
515
+ }
516
+
517
+ function isLocalInstalled() {
518
+ return existsSync(join(PIPELY_DIR, "start.mjs")) && existsSync(join(PIPELY_DIR, "package.json"));
519
+ }
520
+
521
+ function generateLocalEnv() {
522
+ const jwtSecret = generateKey(64);
523
+ const setupKey = require("node:crypto").randomUUID();
524
+
525
+ const env = `# Pipely AI — Local Mode
526
+ DATABASE_URL=file:./data/pipely.db
527
+ JWT_SECRET=${jwtSecret}
528
+ OWNER_SETUP_KEY=${setupKey}
529
+ FRONTEND_URL=http://localhost:3000
530
+ BACKEND_URL=http://localhost:3333
531
+ POLL_INTERVAL_MS=60000
532
+ PORT=3333
533
+ VITE_API_URL=http://localhost:3333
534
+ `;
535
+ return { env, setupKey };
536
+ }
537
+
538
+ async function installLocal() {
539
+ printBanner();
540
+
541
+ const os = detectOS();
542
+ console.log(` Sistema: ${c.bold}${os.label}${c.reset} (${os.arch})`);
543
+ console.log(` Modo: ${c.cyan}Local (sem Docker)${c.reset}\n`);
544
+
545
+ // Check if already installed
546
+ if (isLocalInstalled()) {
547
+ console.log(` ${c.green}✓${c.reset} Pipely AI ja instalado em ${c.dim}${PIPELY_DIR}${c.reset}\n`);
548
+ console.log(` Iniciando...\n`);
549
+ return runLocal();
550
+ }
551
+
552
+ // Download bundle
553
+ console.log(` ${c.magenta}── Baixando Pipely AI ─────────────────────${c.reset}\n`);
554
+
555
+ process.stdout.write(` Buscando ultima versao... `);
556
+ const bundleUrl = await getLatestReleaseUrl();
557
+
558
+ if (!bundleUrl) {
559
+ console.log(`${c.red}✗${c.reset}`);
560
+ console.log(`\n ${c.red}Bundle nao encontrado no GitHub Releases.${c.reset}`);
561
+ console.log(` ${c.dim}Verifique: https://github.com/${BUNDLE_REPO}/releases${c.reset}\n`);
562
+ process.exit(1);
563
+ }
564
+ console.log(`${c.green}✓${c.reset}`);
565
+
566
+ const tmpFile = join(PIPELY_DIR, BUNDLE_NAME);
567
+ const { mkdirSync: mk } = require("node:fs");
568
+ mk(PIPELY_DIR, { recursive: true });
569
+
570
+ process.stdout.write(` Baixando bundle... `);
571
+ try {
572
+ await downloadFile(bundleUrl, tmpFile);
573
+ console.log(`${c.green}✓${c.reset}`);
574
+ } catch (err) {
575
+ console.log(`${c.red}✗${c.reset}`);
576
+ console.log(` ${c.red}Erro: ${err.message}${c.reset}\n`);
577
+ process.exit(1);
578
+ }
579
+
580
+ // Extract
581
+ process.stdout.write(` Extraindo... `);
582
+ try {
583
+ extractTarGz(tmpFile, PIPELY_DIR);
584
+ console.log(`${c.green}✓${c.reset}`);
585
+ } catch (err) {
586
+ console.log(`${c.red}✗${c.reset}`);
587
+ console.log(` ${c.red}Erro: ${err.message}${c.reset}\n`);
588
+ process.exit(1);
589
+ }
590
+
591
+ // Clean up tar
592
+ try { require("node:fs").unlinkSync(tmpFile); } catch {}
593
+
594
+ // Install deps
595
+ console.log(`\n ${c.magenta}── Instalando dependencias ────────────────${c.reset}\n`);
596
+ try {
597
+ execSync("npm install --production --no-fund --no-audit", {
598
+ cwd: PIPELY_DIR,
599
+ stdio: "inherit",
600
+ });
601
+ console.log(`\n ${c.green}✓${c.reset} Dependencias instaladas`);
602
+ } catch {
603
+ console.log(`\n ${c.red}✗ Erro ao instalar dependencias${c.reset}\n`);
604
+ process.exit(1);
605
+ }
606
+
607
+ // Generate .env
608
+ console.log(`\n ${c.magenta}── Configurando ───────────────────────────${c.reset}\n`);
609
+ const { env: envContent, setupKey } = generateLocalEnv();
610
+ writeFileSync(getLocalEnvPath(), envContent);
611
+ console.log(` ${c.green}✓${c.reset} .env gerado`);
612
+
613
+ // Create data directory
614
+ mk(join(PIPELY_DIR, "data"), { recursive: true });
615
+
616
+ // Setup database
617
+ process.stdout.write(` Criando banco de dados... `);
618
+ try {
619
+ execSync("npx prisma db push --skip-generate", {
620
+ cwd: join(PIPELY_DIR, "server"),
621
+ stdio: "pipe",
622
+ env: { ...process.env, DATABASE_URL: `file:${join(PIPELY_DIR, "data/pipely.db")}` },
623
+ });
624
+ console.log(`${c.green}✓${c.reset}`);
625
+ } catch (err) {
626
+ console.log(`${c.red}✗${c.reset}`);
627
+ console.log(` ${c.dim}${err.message}${c.reset}`);
628
+ }
629
+
630
+ console.log(`\n ${c.green}✓${c.reset} Instalacao concluida\n`);
631
+
632
+ // Print summary and run
633
+ const line = "═".repeat(56);
634
+ console.log(` ${c.green}${line}${c.reset}`);
635
+ console.log(` ${c.green}${c.bold} PIPELY AI — LOCAL MODE${c.reset}`);
636
+ console.log(` ${c.green}${line}${c.reset}`);
637
+ console.log("");
638
+ console.log(` ${c.bold}Setup Key:${c.reset} ${c.yellow}${setupKey}${c.reset}`);
639
+ console.log("");
640
+ console.log(` ${c.bold}Diretorio:${c.reset} ${c.dim}${PIPELY_DIR}${c.reset}`);
641
+ console.log(` ${c.bold}Banco de dados:${c.reset} ${c.dim}${join(PIPELY_DIR, "data/pipely.db")}${c.reset}`);
642
+ console.log("");
643
+
644
+ return runLocal();
645
+ }
646
+
647
+ async function runLocal() {
648
+ if (!isLocalInstalled()) {
649
+ console.log(` ${c.red}✗ Pipely AI nao instalado. Execute: npx pipely-ai${c.reset}\n`);
650
+ process.exit(1);
651
+ }
652
+
653
+ console.log(` ${c.magenta}── Iniciando Pipely AI ────────────────────${c.reset}\n`);
654
+
655
+ const { fork } = require("node:child_process");
656
+ const envPath = getLocalEnvPath();
657
+ const envContent = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
658
+ const envVars = {};
659
+ for (const line of envContent.split("\n")) {
660
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
661
+ if (match) envVars[match[1]] = match[2];
662
+ }
663
+
664
+ const childEnv = { ...process.env, ...envVars };
665
+
666
+ // Start server
667
+ const server = fork(join(PIPELY_DIR, "server/dist/index.js"), [], {
668
+ cwd: join(PIPELY_DIR, "server"),
669
+ env: { ...childEnv, PORT: "3333" },
670
+ stdio: "inherit",
671
+ });
672
+
673
+ // Start agent
674
+ const agent = fork(join(PIPELY_DIR, "agent/dist/index.js"), [], {
675
+ cwd: join(PIPELY_DIR, "agent"),
676
+ env: { ...childEnv, PORT: "3335" },
677
+ stdio: "inherit",
678
+ });
679
+
680
+ // Serve frontend
681
+ const frontendDir = join(PIPELY_DIR, "frontend");
682
+ if (existsSync(frontendDir)) {
683
+ const express = await import("express").catch(() => null);
684
+ if (express) {
685
+ const app = express.default();
686
+ app.use(express.default.static(frontendDir));
687
+ app.get(/^\/(?!api|health).*/, (_req, res) => {
688
+ res.sendFile(join(frontendDir, "index.html"));
689
+ });
690
+ const fPort = envVars.FRONTEND_PORT || 3000;
691
+ app.listen(fPort, () => {
692
+ console.log(` ${c.cyan}Frontend:${c.reset} http://localhost:${fPort}`);
693
+ console.log(` ${c.cyan}Backend:${c.reset} http://localhost:3333`);
694
+ console.log(` ${c.cyan}Agent:${c.reset} http://localhost:3335`);
695
+ console.log("");
696
+ console.log(` Pressione ${c.bold}Ctrl+C${c.reset} para parar.\n`);
697
+ });
698
+ }
699
+ }
700
+
701
+ function shutdown() {
702
+ console.log(`\n Parando...\n`);
703
+ server.kill();
704
+ agent.kill();
705
+ process.exit(0);
706
+ }
707
+
708
+ process.on("SIGINT", shutdown);
709
+ process.on("SIGTERM", shutdown);
710
+ server.on("exit", (code) => { if (code) { agent.kill(); process.exit(1); } });
711
+ agent.on("exit", (code) => { if (code) { server.kill(); process.exit(1); } });
712
+
713
+ // Keep process alive
714
+ await new Promise(() => {});
715
+ }
716
+
449
717
  // ── Banner ──────────────────────────────────────────
450
718
 
451
719
  function printBanner() {
@@ -697,28 +965,27 @@ async function install() {
697
965
 
698
966
  process.stdout.write(` Verificando Docker... `);
699
967
  const docker = checkDocker();
968
+
700
969
  if (!docker.ok) {
701
- printDockerHelp(os);
702
- process.exit(1);
970
+ console.log(`${c.yellow}✗ Nao encontrado${c.reset}`);
971
+ console.log(`\n Docker nao detectado. Iniciando em ${c.cyan}modo local${c.reset} (SQLite, sem containers).\n`);
972
+ return installLocal();
703
973
  }
704
974
  console.log(`${c.green}✓${c.reset} v${docker.version}`);
705
975
 
706
976
  const composeCmd = getComposeCmd();
707
977
  if (!composeCmd) {
708
- console.log(`\n ${c.red}✗ Docker Compose nao encontrado${c.reset}\n`);
709
- process.exit(1);
978
+ console.log(`\n ${c.yellow}✗ Docker Compose nao encontrado${c.reset}`);
979
+ console.log(` Iniciando em ${c.cyan}modo local${c.reset}.\n`);
980
+ return installLocal();
710
981
  }
711
982
 
712
983
  try {
713
984
  execSync("docker info", { stdio: "pipe" });
714
985
  } catch {
715
- console.log(`\n ${c.red}✗ Docker nao esta rodando${c.reset}`);
716
- if (os.platform === "win32" || os.platform === "darwin") {
717
- console.log(` Abra o Docker Desktop e tente novamente.\n`);
718
- } else {
719
- console.log(` Execute: sudo systemctl start docker\n`);
720
- }
721
- process.exit(1);
986
+ console.log(`\n ${c.yellow}✗ Docker nao esta rodando${c.reset}`);
987
+ console.log(` Iniciando em ${c.cyan}modo local${c.reset}.\n`);
988
+ return installLocal();
722
989
  }
723
990
 
724
991
  console.log("");
@@ -878,30 +1145,85 @@ async function install() {
878
1145
  // ── ROUTER ───────────────────────────────────────────
879
1146
  // ══════════════════════════════════════════════════════
880
1147
 
1148
+ // ── Local-mode commands ─────────────────────────────
1149
+
1150
+ function cmdLocalKeys() {
1151
+ if (!isLocalInstalled()) {
1152
+ console.log(`\n ${c.red}✗ Pipely AI nao instalado localmente${c.reset}\n`);
1153
+ process.exit(1);
1154
+ }
1155
+ const env = readEnvFile(PIPELY_DIR);
1156
+ console.log("");
1157
+ console.log(` ${c.bold}PIPELY AI — Chaves (Local)${c.reset}\n`);
1158
+ if (env.OWNER_SETUP_KEY) {
1159
+ console.log(` Setup Key: ${c.yellow}${env.OWNER_SETUP_KEY}${c.reset}`);
1160
+ }
1161
+ if (env.JWT_SECRET) {
1162
+ console.log(` JWT Secret: ${c.yellow}${env.JWT_SECRET.slice(0, 16)}...${c.reset}`);
1163
+ }
1164
+ console.log(`\n Diretorio: ${c.dim}${PIPELY_DIR}${c.reset}`);
1165
+ console.log("");
1166
+ }
1167
+
1168
+ function cmdLocalStart() {
1169
+ if (!isLocalInstalled()) {
1170
+ console.log(`\n ${c.red}✗ Pipely AI nao instalado. Execute: npx pipely-ai${c.reset}\n`);
1171
+ process.exit(1);
1172
+ }
1173
+ runLocal().catch((err) => {
1174
+ console.error(`\n ${c.red}Erro: ${err.message}${c.reset}\n`);
1175
+ process.exit(1);
1176
+ });
1177
+ }
1178
+
1179
+ function isLocalMode() {
1180
+ // Local mode if: local install exists AND no Docker project found
1181
+ return isLocalInstalled() && !findProjectDir();
1182
+ }
1183
+
1184
+ // ══════════════════════════════════════════════════════
1185
+ // ── ROUTER ───────────────────────────────────────────
1186
+ // ══════════════════════════════════════════════════════
1187
+
881
1188
  const args = process.argv.slice(2);
882
1189
  const command = args[0];
1190
+ const local = isLocalMode();
883
1191
 
884
1192
  switch (command) {
885
1193
  case "status":
886
- cmdStatus();
1194
+ if (local) { cmdLocalKeys(); } else { cmdStatus(); }
887
1195
  break;
888
1196
  case "keys":
889
- cmdKeys();
1197
+ if (local) { cmdLocalKeys(); } else { cmdKeys(); }
890
1198
  break;
891
1199
  case "logs":
892
- cmdLogs(args[1]);
1200
+ if (local) { console.log(`\n ${c.dim}Logs aparecem no terminal ao iniciar com: npx pipely-ai start${c.reset}\n`); }
1201
+ else { cmdLogs(args[1]); }
893
1202
  break;
894
1203
  case "stop":
895
- cmdStop();
1204
+ if (local) { console.log(`\n ${c.dim}Pressione Ctrl+C no terminal onde esta rodando${c.reset}\n`); }
1205
+ else { cmdStop(); }
896
1206
  break;
897
1207
  case "start":
898
- cmdStart();
1208
+ if (local) { cmdLocalStart(); } else { cmdStart(); }
899
1209
  break;
900
1210
  case "restart":
901
- cmdRestart();
1211
+ if (local) { console.log(`\n ${c.dim}Pressione Ctrl+C e rode novamente: npx pipely-ai start${c.reset}\n`); }
1212
+ else { cmdRestart(); }
902
1213
  break;
903
1214
  case "update":
904
- cmdUpdate();
1215
+ if (local) {
1216
+ // Delete and re-download bundle
1217
+ const { rmSync: rm } = require("node:fs");
1218
+ try { rm(PIPELY_DIR, { recursive: true }); } catch {}
1219
+ console.log(`\n ${c.dim}Reinstalando...${c.reset}\n`);
1220
+ installLocal().catch((err) => {
1221
+ console.error(`\n ${c.red}Erro: ${err.message}${c.reset}\n`);
1222
+ process.exit(1);
1223
+ });
1224
+ } else {
1225
+ cmdUpdate();
1226
+ }
905
1227
  break;
906
1228
  case "help":
907
1229
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipely-ai",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Pipely AI — Instale, gerencie e atualize com um unico comando",
5
5
  "type": "module",
6
6
  "bin": {