UNPKG

research-cli

Version:

AI-powered research assistant with web search capabilities and beautiful terminal UI

329 lines • 12.1 kB
/** * Auth subcommand application - Typer-inspired structure * * This module creates a Typer-style subcommand app for authentication * with clean separation of concerns and type safety. */ import * as os from "node:os"; import { Command } from "@commander-js/extra-typings"; import { apiKeyManager, } from "../auth/keyring-manager.js"; import { auth } from "../ui/formatters.js"; import { InteractiveUI } from "../ui/interactive.js"; import { getOpenAIApiKey, hasStoredApiKey, removeStoredApiKey, } from "../utils/credentials.js"; /** * Create the auth subcommand application * This follows Typer's pattern of creating modular command apps */ export function createAuthApp() { const authCommand = new Command("auth").description("šŸ” Manage API keys and authentication"); // Legacy commands for backward compatibility authCommand .command("status") .description("Check API key status") .action(async () => { await handleStatus(); }); authCommand .command("setup") .description("Set up OpenAI API key interactively") .action(async () => { await handleSetup(); }); authCommand .command("remove") .description("Remove stored API key from keychain") .action(async () => { await handleRemove(); }); // Modern commands using SecureAPIKeyManager authCommand .command("login") .description("Set up API key for a provider") .argument("<provider>", "Provider to authenticate with (openai, claude, gemini, perplexity)") .option("--key <key>", "API key to store") .action(async (provider, options) => { await handleLogin(provider, options); }); authCommand .command("logout") .description("Remove stored API key for a provider") .argument("<provider>", "Provider to remove (openai, claude, gemini, perplexity)") .option("--force", "Skip confirmation prompt") .action(async (provider, options) => { await handleLogout(provider, options); }); authCommand .command("list") .description("Show authentication status for all providers") .action(async () => { await handleList(); }); authCommand .command("test") .description("Test API connection for a provider") .argument("<provider>", "Provider to test (openai, claude, gemini, perplexity)") .action(async (provider) => { await handleTest(provider); }); authCommand .command("migrate") .description("Migrate API keys from config file to secure storage") .option("--config <path>", "Config file to migrate from", "~/.research-cli/config.json") .action(async (options) => { await handleMigrate(options); }); authCommand .command("keyring-status") .description("Check system keyring availability") .action(async () => { await handleKeyringStatus(); }); return authCommand; } // Command handlers - Clean separation following Typer patterns async function handleStatus() { try { const hasStored = await hasStoredApiKey(); const hasEnv = !!process.env["OPENAI_API_KEY"]; console.log(auth.heading("šŸ”‘ API Key Status")); if (hasEnv) { console.log(auth.success("Environment variable: OPENAI_API_KEY is set")); } else { console.log(auth.error("Environment variable: OPENAI_API_KEY not set")); } if (hasStored) { console.log(auth.success("Keychain: API key is stored securely")); } else { console.log(auth.error("Keychain: No API key stored")); } if (!hasEnv && !hasStored) { console.log(auth.info("šŸ’” Run a query to be prompted for your API key, or set OPENAI_API_KEY environment variable")); } } catch (error) { console.error("Error checking API key status:", error instanceof Error ? error.message : "Unknown error"); } } async function handleSetup() { try { console.log("šŸ”§ Setting up OpenAI API key..."); await getOpenAIApiKey(); console.log("āœ… API key setup complete!"); } catch (error) { console.error("Error setting up API key:", error instanceof Error ? error.message : "Unknown error"); process.exit(1); } } async function handleRemove() { try { const hasStored = await hasStoredApiKey(); if (!hasStored) { console.log("āŒ No API key is stored in keychain"); return; } const removed = await removeStoredApiKey(); if (removed) { console.log("āœ… API key removed from keychain"); } else { console.log("āŒ Failed to remove API key from keychain"); } } catch (error) { console.error("Error removing API key:", error instanceof Error ? error.message : "Unknown error"); } } async function handleLogin(provider, options) { try { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(provider)) { console.error(`āŒ Invalid provider: ${provider}`); console.log(`Valid providers: ${validProviders.join(", ")}`); process.exit(1); } const providerKey = provider; if (options.key) { // Store provided key const success = await apiKeyManager.storeApiKey(providerKey, options.key); if (success) { console.log(`āœ… Successfully authenticated with ${provider}`); } else { console.error(`āŒ Failed to store API key for ${provider}`); process.exit(1); } } else { // Interactive setup const apiKey = await apiKeyManager.setupApiKey(providerKey); if (apiKey) { console.log(`āœ… Successfully authenticated with ${provider}`); } else { console.error(`āŒ Failed to set up API key for ${provider}`); process.exit(1); } } // Test the key const testResult = await apiKeyManager.testApiKey(providerKey); if (testResult.valid) { console.log(`āœ… API key verified and working`); } else { console.log(`āš ļø API key stored but validation failed: ${testResult.error}`); } } catch (error) { console.error("Error setting up API key:", error instanceof Error ? error.message : "Unknown error"); process.exit(1); } } async function handleLogout(provider, options) { try { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(provider)) { console.error(`āŒ Invalid provider: ${provider}`); console.log(`Valid providers: ${validProviders.join(", ")}`); process.exit(1); } const providerKey = provider; const hasKey = await apiKeyManager.getApiKey(providerKey); if (!hasKey) { console.log(`ā„¹ļø No API key stored for ${provider}`); return; } if (!options.force) { const shouldRemove = await InteractiveUI.confirmAction(`Remove API key for ${provider}?`, false); if (!shouldRemove) { console.log("\nšŸ‘‹ Cancelled"); return; } } const success = await apiKeyManager.removeApiKey(providerKey); if (!success) { console.error(`āŒ Failed to remove API key for ${provider}`); process.exit(1); } } catch (error) { console.error("Error removing API key:", error instanceof Error ? error.message : "Unknown error"); } } async function handleList() { try { console.log("\nšŸ”‘ Authentication Status:"); const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; const storedProviders = await apiKeyManager.listStoredProviders(); if (storedProviders.length === 0) { console.log("āŒ No API keys stored"); console.log("Run: research-cli auth login <provider>"); return; } for (const provider of validProviders) { const hasKey = storedProviders.includes(provider); const status = hasKey ? "āœ… Configured" : "āŒ Not configured"; console.log(` ${provider.padEnd(12)} ${status}`); } console.log(`\nTotal providers configured: ${storedProviders.length}`); } catch (error) { console.error("Error checking authentication status:", error instanceof Error ? error.message : "Unknown error"); } } async function handleTest(provider) { try { const validProviders = [ "openai", "claude", "gemini", "perplexity", ]; if (!validProviders.includes(provider)) { console.error(`āŒ Invalid provider: ${provider}`); console.log(`Valid providers: ${validProviders.join(", ")}`); process.exit(1); } const providerKey = provider; const apiKey = await apiKeyManager.getApiKey(providerKey); if (!apiKey) { console.error(`āŒ No API key stored for ${provider}`); console.log(`Run: research-cli auth login ${provider}`); process.exit(1); } console.log(`Testing ${provider} API connection...`); const testResult = await apiKeyManager.testApiKey(providerKey); if (testResult.valid) { console.log(`āœ… ${provider} API key is valid`); } else { console.error(`āŒ ${provider} API test failed: ${testResult.error}`); console.log(`Run: research-cli auth login ${provider} to update your key`); } } catch (error) { console.error("Error testing API key:", error instanceof Error ? error.message : "Unknown error"); } } async function handleMigrate(options) { try { const configPath = options.config.replace("~", os.homedir()); console.log(`Migrating API keys from: ${configPath}`); const result = await apiKeyManager.migrateFromConfig(configPath); if (result.migrated > 0) { console.log(`āœ… Successfully migrated ${result.migrated} API keys`); if (result.errors.length > 0) { console.log("āš ļø Some migrations failed:"); result.errors.forEach((error) => console.log(` - ${error}`)); } } else { console.log("ā„¹ļø No API keys found to migrate"); if (result.errors.length > 0) { console.log("Errors:"); result.errors.forEach((error) => console.log(` - ${error}`)); } } } catch (error) { console.error("Error migrating API keys:", error instanceof Error ? error.message : "Unknown error"); } } async function handleKeyringStatus() { try { console.log("šŸ” Checking system keyring status..."); const testResult = await apiKeyManager.testKeyringAvailability(); if (testResult.available) { console.log("āœ… System keyring is available and working"); console.log(`Backend: ${testResult.backend}`); } else { console.log("āŒ System keyring is not available"); if (testResult.error) { console.log(`Error: ${testResult.error}`); } console.log("Will use encrypted fallback storage"); } } catch (error) { console.error("Error checking keyring status:", error instanceof Error ? error.message : "Unknown error"); } } //# sourceMappingURL=auth-app.js.map