UNPKG

safevibe

Version:

Safevibe CLI - Simple personal secret vault for AI developers and amateur vibe coders

250 lines (249 loc) • 11.1 kB
import chalk from "chalk"; import ora from "ora"; import inquirer from "inquirer"; import open from "open"; import { initConfig, isInitialized, KEYS_FILE } from "../utils/config.js"; import { generateKeyPair, saveKeyPair } from "../crypto/keys.js"; import { checkMcpServer, getCursorInstructions } from "../utils/mcp.js"; /** * Initialize Safevibe in the current project with Google OAuth authentication */ export async function initCommand(options) { console.log(chalk.cyan("🌿 Initializing Safevibe...\n")); try { // Check if already initialized if (!options.force && (await isInitialized())) { console.log(chalk.yellow("āš ļø Safevibe is already initialized.")); const { proceed } = await inquirer.prompt([ { type: "confirm", name: "proceed", message: "Reconfigure Safevibe?", default: false, }, ]); if (!proceed) { console.log(chalk.blue("šŸ‘‹ Initialization cancelled.")); return; } options.force = true; } // Get backend URL if not provided let backendUrl = options.backendUrl; if (!backendUrl) { const { url } = await inquirer.prompt([ { type: "input", name: "url", message: "Backend URL:", default: "https://safevibe-backend.vercel.app", validate: (input) => { try { new URL(input); return true; } catch { return "Please enter a valid URL"; } }, }, ]); backendUrl = url; } // Test backend connection const connectionSpinner = ora("Testing backend connection...").start(); try { const response = await fetch(`${backendUrl}/api/health`, { method: "GET", headers: { "Content-Type": "application/json" }, }); if (!response.ok) { connectionSpinner.fail("Backend connection failed"); console.log(chalk.red(`āŒ Backend at ${backendUrl} is not responding`)); process.exit(1); } connectionSpinner.succeed("Backend connection verified"); } catch (error) { connectionSpinner.fail("Backend connection failed"); console.log(chalk.red(`āŒ Cannot connect to ${backendUrl}`)); process.exit(1); } // Step 1: Request device authorization codes console.log(chalk.cyan("\nšŸ” Setting up Google OAuth authentication...")); const authSpinner = ora("Requesting authorization...").start(); let deviceResponse; try { const response = await fetch(`${backendUrl}/api/trpc/auth.requestDeviceCode`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ json: {} }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); deviceResponse = data.result?.data?.json || data.result?.data; authSpinner.succeed("Authorization request ready"); } catch (error) { authSpinner.fail("Failed to request authorization"); throw error; } // Step 2: Display user instructions console.log(chalk.cyan("\nšŸ“± Google Authentication Required")); console.log(chalk.white("Please authenticate with your Google account to continue.\n")); console.log(chalk.yellow("Follow these steps:")); console.log(chalk.white(` 1. Copy this code: ${chalk.bold.cyan(deviceResponse.userCode)}`)); console.log(chalk.white(` 2. Visit: ${chalk.underline.blue(deviceResponse.verificationUri.trim())}`)); console.log(chalk.white(" 3. Sign in and enter the code")); const { openBrowser } = await inquirer.prompt([ { type: "confirm", name: "openBrowser", message: "Open the authorization page in your browser?", default: true, }, ]); if (openBrowser) { try { const cleanUrl = deviceResponse.verificationUriComplete.trim(); await open(cleanUrl); console.log(chalk.green("āœ… Browser opened")); } catch (error) { console.log(chalk.yellow("āš ļø Please manually visit the URL above")); } } // Step 3: Poll for authorization completion const pollSpinner = ora("Waiting for authentication...").start(); let accessToken; let userId; const maxAttempts = Math.floor(deviceResponse.expiresIn / deviceResponse.interval); let attempts = 0; while (attempts < maxAttempts) { try { const response = await fetch(`${backendUrl}/api/trpc/auth.pollDeviceCode`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ json: { deviceCode: deviceResponse.deviceCode } }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); const pollResult = data.result?.data?.json || data.result?.data; accessToken = pollResult.accessToken; userId = pollResult.userId; pollSpinner.succeed("Authentication completed!"); break; } catch (error) { if (error.message && error.message.includes("HTTP 500")) { try { const response = await fetch(`${backendUrl}/api/trpc/auth.pollDeviceCode`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ json: { deviceCode: deviceResponse.deviceCode } }), }); if (!response.ok) { const errorData = await response.json(); const actualErrorMessage = errorData.error?.json?.message || "Unknown error"; if (actualErrorMessage === "Authorization pending") { attempts++; await new Promise(resolve => setTimeout(resolve, deviceResponse.interval * 1000)); continue; } else if (actualErrorMessage === "Device code expired") { pollSpinner.fail("Authentication timed out"); console.log(chalk.red("āŒ Authentication code expired")); console.log(chalk.yellow("šŸ’” Please run 'safevibe init' again")); process.exit(1); } else { pollSpinner.fail("Authentication failed"); console.log(chalk.red(`āŒ Error: ${actualErrorMessage}`)); throw error; } } } catch (fetchError) { pollSpinner.fail("Authentication failed"); throw error; } } else { pollSpinner.fail("Authentication failed"); throw error; } } } if (!accessToken) { pollSpinner.fail("Authentication timed out"); console.log(chalk.red("āŒ Authentication timed out")); process.exit(1); } // Step 4: Initialize configuration const configSpinner = ora("Setting up configuration...").start(); try { await initConfig({ backendUrl, sessionToken: accessToken, userId: userId, force: options.force, }); configSpinner.succeed("Configuration saved"); } catch (error) { configSpinner.fail("Failed to create configuration"); throw error; } // Step 5: Generate encryption keys const keySpinner = ora("Generating encryption keys...").start(); try { const keyPair = generateKeyPair(); await saveKeyPair(keyPair, KEYS_FILE); keySpinner.succeed("Encryption keys generated"); } catch (error) { keySpinner.fail("Failed to generate keys"); throw error; } // Step 6: Check MCP server const mcpSpinner = ora("Checking MCP server...").start(); try { const mcpExists = await checkMcpServer(); if (mcpExists) { mcpSpinner.succeed("MCP server ready"); } else { mcpSpinner.warn("MCP server not found - run 'npm run build' first"); } } catch (error) { mcpSpinner.warn("MCP server check failed"); } // Success message console.log(chalk.green("\nšŸŽ‰ Safevibe initialized successfully!")); console.log(chalk.white(" āœ… Google authentication completed")); console.log(chalk.white(" āœ… Encryption keys generated")); console.log(chalk.white(" āœ… Configuration saved")); console.log(chalk.cyan("\nšŸš€ Next Steps:")); console.log(chalk.white(" 1. Run: safevibe start")); console.log(chalk.white(" 2. Configure Cursor (see instructions below)")); console.log(chalk.white(" 3. Ask Cursor to manage your secrets")); // Show Cursor integration instructions at the bottom console.log(getCursorInstructions()); } catch (error) { console.error(chalk.red(`\nāŒ Initialization failed: ${error.message}`)); console.log(chalk.yellow("\nšŸ’” Troubleshooting:")); console.log(chalk.gray(" - Check write permissions to ~/.safevibe/")); console.log(chalk.gray(" - Verify backend URL is accessible")); console.log(chalk.gray(" - Complete the Google OAuth flow")); console.log(chalk.gray(" - Try running with --force flag")); process.exit(1); } }