safevibe
Version:
Safevibe CLI - Simple personal secret vault for AI developers and amateur vibe coders
250 lines (249 loc) ⢠11.1 kB
JavaScript
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);
}
}