research-cli
Version:
AI-powered research assistant with web search capabilities and beautiful terminal UI
329 lines ⢠12.1 kB
JavaScript
/**
* 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