#!/usr/bin/env node /** * OpenMonetis Setup Script * Uso: node setup.mjs */ import { createInterface } from "readline"; import { execSync } from "child_process"; import { writeFileSync, existsSync } from "fs"; import { randomBytes } from "crypto"; import { resolve, join } from "path"; // ─── Cores e símbolos ──────────────────────────────────────────────────────── const c = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", red: "\x1b[31m", yellow: "\x1b[33m", cyan: "\x1b[36m", orange: "\x1b[38;5;214m", }; const sym = { ok: `${c.green}✔${c.reset}`, fail: `${c.red}✗${c.reset}`, warn: `${c.yellow}!${c.reset}`, arrow: `${c.cyan}→${c.reset}`, }; // ─── Helpers ───────────────────────────────────────────────────────────────── function section(label) { console.log(`\n${c.dim}── ${label} ${"─".repeat(Math.max(0, 48 - label.length))}${c.reset}`); } function runSilent(cmd) { try { return execSync(cmd, { stdio: "pipe" }).toString().trim(); } catch { return null; } } function run(cmd, opts = {}) { execSync(cmd, { stdio: "pipe", ...opts }); } function spinner(text) { const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; let i = 0; const id = setInterval(() => { process.stdout.write(`\r${c.cyan}${frames[i++ % frames.length]}${c.reset} ${text}`); }, 80); return { stop: (msg) => { clearInterval(id); process.stdout.write(`\r${sym.ok} ${msg}\n`); }, fail: (msg) => { clearInterval(id); process.stdout.write(`\r${sym.fail} ${msg}\n`); }, }; } const rl = createInterface({ input: process.stdin, output: process.stdout }); const ask = (q) => new Promise((res) => rl.question(q, res)); async function askDefault(question, defaultValue) { const answer = await ask(`${question} [${c.dim}${defaultValue}${c.reset}]: `); return answer.trim() || defaultValue; } async function askYesNo(question) { const answer = await ask(`${question} ${c.dim}[s/N]${c.reset}: `); return answer.trim().toLowerCase() === "s"; } function abort(msg) { console.log(`\n${sym.fail} ${msg}\n`); rl.close(); process.exit(1); } // ─── Header ────────────────────────────────────────────────────────────────── const logoLines = [ ".............................+@@@@@@@@@@=.............................", ".............................@@@@@@@@@@@:.............................", "...................+@@@@@@*-:@@@@@@@@@@%...=@@@@@@-...................", "..................@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%..................", "................=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+................", "...................-=+%@@@@@@@@@@@@@@@@@@@@@*:........................", ".......................#@@@@@@@@@@@@@@@@@@@@@@@+......................", "....................%@@@@@@@@@@@@@%#@@@@@@@@@@@@*.....................", "....................+@@@@@@@@@@@......*@@@@@@#........................", ".........................:#@@=...........+#...........................", ]; const nameLines = [ " ___ __ __ _ _ ", " / _ \\ _ __ ___ _ __ | \\/ | ___ _ __ ___| |_(_)___ ", " | | | | '_ \\ / _ \\ '_ \\| |\\/| |/ _ \\| '_ \\ / _ \\ __| / __|", " | |_| | |_) | __/ | | | | | | (_) | | | | __/ |_| \\__ \\", " \\___/| .__/ \\___|_| |_|_| |_|\\___/|_| |_|\\___|\\__|_|___/", " |_| ", ]; const nameStart = Math.floor((logoLines.length - nameLines.length) / 2); console.log(); for (let i = 0; i < logoLines.length; i++) { const logoCol = c.orange + logoLines[i].replaceAll(".", " ").substring(14, 56).padEnd(42) + c.reset; const nameIdx = i - nameStart; const nameCol = nameIdx >= 0 && nameIdx < nameLines.length ? nameLines[nameIdx] : ""; console.log(logoCol + " " + nameCol); } console.log(`\n${" ".repeat(46)}${c.dim}Gestão financeira · self-hosted${c.reset}\n`); // ─── ETAPA 1: Verificações do sistema ──────────────────────────────────────── section("Verificando sistema"); // Node const nodeMajor = parseInt(process.versions.node.split(".")[0]); if (nodeMajor < 22) { console.log(`${sym.fail} Node.js ${process.versions.node} — requer 22+`); console.log(` ${sym.arrow} https://nodejs.org`); process.exit(1); } console.log(`${sym.ok} Node.js ${process.versions.node}`); // pnpm let pnpmVersion = runSilent("pnpm --version"); if (!pnpmVersion) { process.stdout.write(`${sym.warn} pnpm não encontrado — instalando... `); try { run("npm install -g pnpm"); pnpmVersion = runSilent("pnpm --version"); process.stdout.write(`${sym.ok}\n`); console.log(`${sym.ok} pnpm ${pnpmVersion}`); } catch { console.log(`\n${sym.fail} Falha ao instalar pnpm`); console.log(` ${sym.arrow} npm install -g pnpm`); process.exit(1); } } else { console.log(`${sym.ok} pnpm ${pnpmVersion}`); } // Git if (!runSilent("git --version")) { console.log(`${sym.fail} Git não encontrado`); console.log(` ${sym.arrow} https://git-scm.com`); process.exit(1); } console.log(`${sym.ok} Git disponível`); // Docker const dockerAvailable = !!runSilent("docker --version"); if (dockerAvailable) { console.log(`${sym.ok} Docker disponível`); } else { console.log(`${sym.warn} Docker não encontrado — banco local indisponível`); } // ─── ETAPA 2: Banco de dados ────────────────────────────────────────────────── section("Banco de dados"); let databaseUrl; let useLocalDocker = false; if (dockerAvailable) { console.log(` [1] PostgreSQL local via Docker ${c.dim}(recomendado)${c.reset}`); console.log(` [2] URL remota ${c.dim}(Supabase, Neon, Railway...)${c.reset}\n`); const dbChoice = await ask(`Escolha [1]: `); if (dbChoice.trim() === "2") { databaseUrl = await ask(`DATABASE_URL: `); if (!databaseUrl.match(/^postgre(s|sql):\/\//)) { abort("URL inválida — deve começar com postgresql:// ou postgres://"); } } else { useLocalDocker = true; databaseUrl = "postgresql://openmonetis:openmonetis_dev_password@localhost:5432/openmonetis_db"; console.log(`${sym.ok} Banco local selecionado`); } } else { console.log(` ${c.dim}Insira a URL de um banco remoto (Supabase, Neon, Railway...)${c.reset}\n`); databaseUrl = await ask(`DATABASE_URL: `); if (!databaseUrl.match(/^postgre(s|sql):\/\//)) { abort("URL inválida — deve começar com postgresql:// ou postgres://"); } } // ─── ETAPA 3: Autenticação ──────────────────────────────────────────────────── section("Autenticação"); const authSecret = randomBytes(32).toString("base64"); const betterAuthUrl = await askDefault("URL da aplicação", "http://localhost:3000"); console.log(`${sym.ok} BETTER_AUTH_SECRET gerado`); console.log(`${sym.ok} BETTER_AUTH_URL: ${betterAuthUrl}`); // ─── ETAPA 4: Opcionais ─────────────────────────────────────────────────────── section("Opcionais"); console.log(` ${c.dim}Deixe em branco e configure depois editando o .env${c.reset}\n`); // Google OAuth let googleClientId = ""; let googleClientSecret = ""; if (await askYesNo(" Google OAuth (login social)?")) { googleClientId = await ask(" GOOGLE_CLIENT_ID: "); googleClientSecret = await ask(" GOOGLE_CLIENT_SECRET: "); } // Resend let resendApiKey = ""; let resendFromEmail = ""; if (await askYesNo(" E-mail via Resend (notificações e convites)?")) { resendApiKey = await ask(" RESEND_API_KEY: "); resendFromEmail = await ask(` RESEND_FROM_EMAIL [OpenMonetis ]: `); if (!resendFromEmail.trim()) resendFromEmail = "OpenMonetis "; } // AI let anthropicKey = ""; let openaiKey = ""; let googleAiKey = ""; let openrouterKey = ""; if (await askYesNo(" Insights com IA (Claude, GPT, Gemini, OpenRouter)?")) { console.log(` ${c.dim}Deixe em branco o que não for usar${c.reset}`); anthropicKey = await ask(" ANTHROPIC_API_KEY: "); openaiKey = await ask(" OPENAI_API_KEY: "); googleAiKey = await ask(" GOOGLE_GENERATIVE_AI_API_KEY: "); openrouterKey = await ask(" OPENROUTER_API_KEY: "); } // Domínio público let publicDomain = ""; if (await askYesNo(" Domínio público separado para a landing page?")) { publicDomain = await ask(" PUBLIC_DOMAIN (ex: openmonetis.com): "); } rl.close(); // ─── ETAPA 5: Confirmar e executar ──────────────────────────────────────────── const targetDir = resolve("openmonetis"); section("Instalação"); console.log(` ${sym.arrow} Clonar repositório em ./openmonetis ${sym.arrow} Gerar .env ${sym.arrow} pnpm install${useLocalDocker ? `\n ${sym.arrow} Subir banco PostgreSQL (Docker)\n ${sym.arrow} Habilitar extensões` : ""} ${sym.arrow} pnpm db:push `); if (existsSync(targetDir)) { abort("A pasta ./openmonetis já existe. Remova-a e tente novamente."); } // Clonar let s = spinner("Clonando repositório..."); try { run("git clone https://github.com/felipegcoutinho/openmonetis.git openmonetis"); s.stop("Repositório clonado"); } catch { s.fail("Falha ao clonar repositório"); process.exit(1); } // Gerar .env const val = (v, fallback = "") => v?.trim() || fallback; const opt = (key, value) => (value?.trim() ? `${key}=${value}` : `# ${key}=`); const envContent = [ `# Gerado por setup.mjs em ${new Date().toISOString()}`, "", "# === Database ===", `DATABASE_URL=${databaseUrl}`, "", "# === Better Auth ===", `BETTER_AUTH_SECRET=${authSecret}`, `BETTER_AUTH_URL=${betterAuthUrl}`, "", "# === Portas ===", "APP_PORT=3000", "DB_PORT=5432", "", "# === PostgreSQL (Docker local) ===", "POSTGRES_USER=openmonetis", "POSTGRES_PASSWORD=openmonetis_dev_password", "POSTGRES_DB=openmonetis_db", "", "# === Multi-domínio ===", opt("PUBLIC_DOMAIN", publicDomain), "", "# === Google OAuth ===", opt("GOOGLE_CLIENT_ID", googleClientId), opt("GOOGLE_CLIENT_SECRET", googleClientSecret), "", "# === Email (Resend) ===", opt("RESEND_API_KEY", resendApiKey), resendFromEmail ? `RESEND_FROM_EMAIL="${resendFromEmail}"` : "# RESEND_FROM_EMAIL=", "", "# === AI Providers ===", opt("ANTHROPIC_API_KEY", anthropicKey), opt("OPENAI_API_KEY", openaiKey), opt("GOOGLE_GENERATIVE_AI_API_KEY", googleAiKey), opt("OPENROUTER_API_KEY", openrouterKey), ].join("\n"); writeFileSync(join(targetDir, ".env"), envContent); console.log(`${sym.ok} .env gerado`); // pnpm install s = spinner("Instalando dependências..."); try { run("pnpm install", { cwd: targetDir }); s.stop("Dependências instaladas"); } catch { s.fail("Falha ao instalar dependências"); process.exit(1); } // Docker local if (useLocalDocker) { s = spinner("Subindo banco PostgreSQL..."); try { run("pnpm docker:up:db", { cwd: targetDir }); s.stop("Banco iniciado"); } catch { s.fail("Falha ao iniciar o banco"); process.exit(1); } // Aguardar postgres ficar pronto s = spinner("Aguardando PostgreSQL ficar pronto..."); let ready = false; for (let i = 0; i < 20; i++) { try { run("docker compose exec -T db pg_isready -U openmonetis", { cwd: targetDir }); ready = true; break; } catch { await new Promise((r) => setTimeout(r, 1500)); } } if (!ready) { s.fail("PostgreSQL não respondeu a tempo"); process.exit(1); } s.stop("PostgreSQL pronto"); // Extensões s = spinner("Habilitando extensões do banco..."); try { run("pnpm db:extensions", { cwd: targetDir }); s.stop("Extensões habilitadas"); } catch { s.fail("Falha ao habilitar extensões"); process.exit(1); } } // db:push s = spinner("Aplicando schema no banco..."); try { run("pnpm db:push", { cwd: targetDir }); s.stop("Schema aplicado"); } catch { s.fail("Falha ao aplicar schema"); process.exit(1); } // ─── Finalização ────────────────────────────────────────────────────────────── console.log(` ${c.green}${c.bold} ✔ OpenMonetis instalado com sucesso!${c.reset} ${c.bold}Para iniciar:${c.reset} cd openmonetis pnpm dev${ useLocalDocker ? ` ${c.dim}→ desenvolvimento${c.reset}\n pnpm docker:up ${c.dim}→ produção local (app + banco)${c.reset}` : ` ${c.dim}→ desenvolvimento${c.reset}` } ${c.bold}Acesse:${c.reset} ${betterAuthUrl} ${c.bold}Docs:${c.reset} https://github.com/felipegcoutinho/openmonetis `);