mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 22:32:04 +00:00
522 lines
17 KiB
TypeScript
522 lines
17 KiB
TypeScript
/**
|
|
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
|
*/
|
|
|
|
import { Router, type Request, type Response } from "express";
|
|
import { exec } from "child_process";
|
|
import { promisify } from "util";
|
|
import os from "os";
|
|
import path from "path";
|
|
import fs from "fs/promises";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
// Storage for API keys (in-memory cache)
|
|
const apiKeys: Record<string, string> = {};
|
|
|
|
// Helper to persist API keys to .env file
|
|
async function persistApiKeyToEnv(key: string, value: string): Promise<void> {
|
|
const envPath = path.join(process.cwd(), ".env");
|
|
|
|
try {
|
|
let envContent = "";
|
|
try {
|
|
envContent = await fs.readFile(envPath, "utf-8");
|
|
} catch {
|
|
// .env file doesn't exist, we'll create it
|
|
}
|
|
|
|
// Parse existing env content
|
|
const lines = envContent.split("\n");
|
|
const keyRegex = new RegExp(`^${key}=`);
|
|
let found = false;
|
|
const newLines = lines.map((line) => {
|
|
if (keyRegex.test(line)) {
|
|
found = true;
|
|
return `${key}=${value}`;
|
|
}
|
|
return line;
|
|
});
|
|
|
|
if (!found) {
|
|
// Add the key at the end
|
|
newLines.push(`${key}=${value}`);
|
|
}
|
|
|
|
await fs.writeFile(envPath, newLines.join("\n"));
|
|
console.log(`[Setup] Persisted ${key} to .env file`);
|
|
} catch (error) {
|
|
console.error(`[Setup] Failed to persist ${key} to .env:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function createSetupRoutes(): Router {
|
|
const router = Router();
|
|
|
|
// Get Claude CLI status
|
|
router.get("/claude-status", async (_req: Request, res: Response) => {
|
|
try {
|
|
let installed = false;
|
|
let version = "";
|
|
let cliPath = "";
|
|
let method = "none";
|
|
|
|
// Try to find Claude CLI
|
|
try {
|
|
const { stdout } = await execAsync("which claude || where claude 2>/dev/null");
|
|
cliPath = stdout.trim();
|
|
installed = true;
|
|
method = "path";
|
|
|
|
// Get version
|
|
try {
|
|
const { stdout: versionOut } = await execAsync("claude --version");
|
|
version = versionOut.trim();
|
|
} catch {
|
|
// Version command might not be available
|
|
}
|
|
} catch {
|
|
// Not in PATH, try common locations
|
|
const commonPaths = [
|
|
path.join(os.homedir(), ".local", "bin", "claude"),
|
|
path.join(os.homedir(), ".claude", "local", "claude"),
|
|
"/usr/local/bin/claude",
|
|
path.join(os.homedir(), ".npm-global", "bin", "claude"),
|
|
];
|
|
|
|
for (const p of commonPaths) {
|
|
try {
|
|
await fs.access(p);
|
|
cliPath = p;
|
|
installed = true;
|
|
method = "local";
|
|
|
|
// Get version from this path
|
|
try {
|
|
const { stdout: versionOut } = await execAsync(`"${p}" --version`);
|
|
version = versionOut.trim();
|
|
} catch {
|
|
// Version command might not be available
|
|
}
|
|
break;
|
|
} catch {
|
|
// Not found at this path
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check authentication - detect all possible auth methods
|
|
// Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth
|
|
// apiKeys.anthropic stores direct API keys for pay-per-use
|
|
let auth = {
|
|
authenticated: false,
|
|
method: "none" as string,
|
|
hasCredentialsFile: false,
|
|
hasToken: false,
|
|
hasStoredOAuthToken: !!apiKeys.anthropic_oauth_token,
|
|
hasStoredApiKey: !!apiKeys.anthropic,
|
|
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
|
|
hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
|
// Additional fields for detailed status
|
|
oauthTokenValid: false,
|
|
apiKeyValid: false,
|
|
hasCliAuth: false,
|
|
hasRecentActivity: false,
|
|
};
|
|
|
|
const claudeDir = path.join(os.homedir(), ".claude");
|
|
|
|
// Check for recent Claude CLI activity - indicates working authentication
|
|
// The stats-cache.json file is only populated when the CLI is working properly
|
|
const statsCachePath = path.join(claudeDir, "stats-cache.json");
|
|
try {
|
|
const statsContent = await fs.readFile(statsCachePath, "utf-8");
|
|
const stats = JSON.parse(statsContent);
|
|
|
|
// Check if there's any activity (which means the CLI is authenticated and working)
|
|
if (stats.dailyActivity && stats.dailyActivity.length > 0) {
|
|
auth.hasRecentActivity = true;
|
|
auth.hasCliAuth = true;
|
|
auth.authenticated = true;
|
|
auth.method = "cli_authenticated";
|
|
}
|
|
} catch {
|
|
// Stats file doesn't exist or is invalid
|
|
}
|
|
|
|
// Check for settings.json - indicates CLI has been set up
|
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
try {
|
|
await fs.access(settingsPath);
|
|
// If settings exist but no activity, CLI might be set up but not authenticated
|
|
if (!auth.hasCliAuth) {
|
|
// Try to check for other indicators of auth
|
|
const sessionsDir = path.join(claudeDir, "projects");
|
|
try {
|
|
const sessions = await fs.readdir(sessionsDir);
|
|
if (sessions.length > 0) {
|
|
auth.hasCliAuth = true;
|
|
auth.authenticated = true;
|
|
auth.method = "cli_authenticated";
|
|
}
|
|
} catch {
|
|
// Sessions directory doesn't exist
|
|
}
|
|
}
|
|
} catch {
|
|
// Settings file doesn't exist
|
|
}
|
|
|
|
// Check for credentials file (OAuth tokens from claude login) - legacy/alternative auth
|
|
const credentialsPath = path.join(claudeDir, "credentials.json");
|
|
try {
|
|
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
|
|
const credentials = JSON.parse(credentialsContent);
|
|
auth.hasCredentialsFile = true;
|
|
|
|
// Check what type of token is in credentials
|
|
if (credentials.oauth_token || credentials.access_token) {
|
|
auth.hasStoredOAuthToken = true;
|
|
auth.oauthTokenValid = true;
|
|
auth.authenticated = true;
|
|
auth.method = "oauth_token"; // Stored OAuth token from credentials file
|
|
} else if (credentials.api_key) {
|
|
auth.apiKeyValid = true;
|
|
auth.authenticated = true;
|
|
auth.method = "api_key"; // Stored API key in credentials file
|
|
}
|
|
} catch {
|
|
// No credentials file or invalid format
|
|
}
|
|
|
|
// Environment variables override stored credentials (higher priority)
|
|
if (auth.hasEnvOAuthToken) {
|
|
auth.authenticated = true;
|
|
auth.oauthTokenValid = true;
|
|
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
|
|
} else if (auth.hasEnvApiKey) {
|
|
auth.authenticated = true;
|
|
auth.apiKeyValid = true;
|
|
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
|
|
}
|
|
|
|
// In-memory stored OAuth token (from setup wizard - subscription auth)
|
|
if (!auth.authenticated && apiKeys.anthropic_oauth_token) {
|
|
auth.authenticated = true;
|
|
auth.oauthTokenValid = true;
|
|
auth.method = "oauth_token"; // Stored OAuth token from setup wizard
|
|
}
|
|
|
|
// In-memory stored API key (from settings UI - pay-per-use)
|
|
if (!auth.authenticated && apiKeys.anthropic) {
|
|
auth.authenticated = true;
|
|
auth.apiKeyValid = true;
|
|
auth.method = "api_key"; // Manually stored API key
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
status: installed ? "installed" : "not_installed",
|
|
installed,
|
|
method,
|
|
version,
|
|
path: cliPath,
|
|
auth,
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Get Codex CLI status
|
|
router.get("/codex-status", async (_req: Request, res: Response) => {
|
|
try {
|
|
let installed = false;
|
|
let version = "";
|
|
let cliPath = "";
|
|
let method = "none";
|
|
|
|
// Try to find Codex CLI
|
|
try {
|
|
const { stdout } = await execAsync("which codex || where codex 2>/dev/null");
|
|
cliPath = stdout.trim();
|
|
installed = true;
|
|
method = "path";
|
|
|
|
try {
|
|
const { stdout: versionOut } = await execAsync("codex --version");
|
|
version = versionOut.trim();
|
|
} catch {
|
|
// Version command might not be available
|
|
}
|
|
} catch {
|
|
// Not found
|
|
}
|
|
|
|
// Check for OpenAI/Codex authentication
|
|
let auth = {
|
|
authenticated: false,
|
|
method: "none" as string,
|
|
hasAuthFile: false,
|
|
hasEnvKey: !!process.env.OPENAI_API_KEY,
|
|
hasStoredApiKey: !!apiKeys.openai,
|
|
hasEnvApiKey: !!process.env.OPENAI_API_KEY,
|
|
// Additional fields for subscription/account detection
|
|
hasSubscription: false,
|
|
cliLoggedIn: false,
|
|
};
|
|
|
|
// Check for OpenAI CLI auth file (~/.codex/auth.json or similar)
|
|
const codexAuthPaths = [
|
|
path.join(os.homedir(), ".codex", "auth.json"),
|
|
path.join(os.homedir(), ".openai", "credentials"),
|
|
path.join(os.homedir(), ".config", "openai", "credentials.json"),
|
|
];
|
|
|
|
for (const authPath of codexAuthPaths) {
|
|
try {
|
|
const authContent = await fs.readFile(authPath, "utf-8");
|
|
const authData = JSON.parse(authContent);
|
|
auth.hasAuthFile = true;
|
|
|
|
// Check for subscription/tokens
|
|
if (authData.subscription || authData.plan || authData.account_type) {
|
|
auth.hasSubscription = true;
|
|
auth.authenticated = true;
|
|
auth.method = "subscription"; // Codex subscription (Plus/Team)
|
|
} else if (authData.access_token || authData.api_key) {
|
|
auth.cliLoggedIn = true;
|
|
auth.authenticated = true;
|
|
auth.method = "cli_verified"; // CLI logged in with account
|
|
}
|
|
break;
|
|
} catch {
|
|
// Auth file not found at this path
|
|
}
|
|
}
|
|
|
|
// Environment variable has highest priority
|
|
if (auth.hasEnvApiKey) {
|
|
auth.authenticated = true;
|
|
auth.method = "env"; // OPENAI_API_KEY environment variable
|
|
}
|
|
|
|
// In-memory stored API key (from settings UI)
|
|
if (!auth.authenticated && apiKeys.openai) {
|
|
auth.authenticated = true;
|
|
auth.method = "api_key"; // Manually stored API key
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
status: installed ? "installed" : "not_installed",
|
|
method,
|
|
version,
|
|
path: cliPath,
|
|
auth,
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Install Claude CLI
|
|
router.post("/install-claude", async (_req: Request, res: Response) => {
|
|
try {
|
|
// In web mode, we can't install CLIs directly
|
|
// Return instructions instead
|
|
res.json({
|
|
success: false,
|
|
error:
|
|
"CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Install Codex CLI
|
|
router.post("/install-codex", async (_req: Request, res: Response) => {
|
|
try {
|
|
res.json({
|
|
success: false,
|
|
error:
|
|
"CLI installation requires terminal access. Please install manually using: npm install -g @openai/codex",
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Auth Claude
|
|
router.post("/auth-claude", async (_req: Request, res: Response) => {
|
|
try {
|
|
res.json({
|
|
success: true,
|
|
requiresManualAuth: true,
|
|
command: "claude login",
|
|
message: "Please run 'claude login' in your terminal to authenticate",
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Auth Codex
|
|
router.post("/auth-codex", async (req: Request, res: Response) => {
|
|
try {
|
|
const { apiKey } = req.body as { apiKey?: string };
|
|
|
|
if (apiKey) {
|
|
apiKeys.openai = apiKey;
|
|
process.env.OPENAI_API_KEY = apiKey;
|
|
res.json({ success: true });
|
|
} else {
|
|
res.json({
|
|
success: true,
|
|
requiresManualAuth: true,
|
|
command: "codex auth login",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Store API key
|
|
router.post("/store-api-key", async (req: Request, res: Response) => {
|
|
try {
|
|
const { provider, apiKey } = req.body as { provider: string; apiKey: string };
|
|
|
|
if (!provider || !apiKey) {
|
|
res.status(400).json({ success: false, error: "provider and apiKey required" });
|
|
return;
|
|
}
|
|
|
|
apiKeys[provider] = apiKey;
|
|
|
|
// Also set as environment variable and persist to .env
|
|
// IMPORTANT: OAuth tokens and API keys must be stored separately
|
|
// - OAuth tokens (subscription auth) -> CLAUDE_CODE_OAUTH_TOKEN
|
|
// - API keys (pay-per-use) -> ANTHROPIC_API_KEY
|
|
if (provider === "anthropic_oauth_token") {
|
|
// OAuth token from claude setup-token (subscription-based auth)
|
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = apiKey;
|
|
await persistApiKeyToEnv("CLAUDE_CODE_OAUTH_TOKEN", apiKey);
|
|
console.log("[Setup] Stored OAuth token as CLAUDE_CODE_OAUTH_TOKEN");
|
|
} else if (provider === "anthropic") {
|
|
// Direct API key (pay-per-use)
|
|
process.env.ANTHROPIC_API_KEY = apiKey;
|
|
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
|
|
console.log("[Setup] Stored API key as ANTHROPIC_API_KEY");
|
|
} else if (provider === "openai") {
|
|
process.env.OPENAI_API_KEY = apiKey;
|
|
await persistApiKeyToEnv("OPENAI_API_KEY", apiKey);
|
|
} else if (provider === "google") {
|
|
process.env.GOOGLE_API_KEY = apiKey;
|
|
await persistApiKeyToEnv("GOOGLE_API_KEY", apiKey);
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Get API keys status
|
|
router.get("/api-keys", async (_req: Request, res: Response) => {
|
|
try {
|
|
res.json({
|
|
success: true,
|
|
hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
|
|
hasOpenAIKey: !!apiKeys.openai || !!process.env.OPENAI_API_KEY,
|
|
hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Configure Codex MCP
|
|
router.post("/configure-codex-mcp", async (req: Request, res: Response) => {
|
|
try {
|
|
const { projectPath } = req.body as { projectPath: string };
|
|
|
|
if (!projectPath) {
|
|
res.status(400).json({ success: false, error: "projectPath required" });
|
|
return;
|
|
}
|
|
|
|
// Create .codex directory and config
|
|
const codexDir = path.join(projectPath, ".codex");
|
|
await fs.mkdir(codexDir, { recursive: true });
|
|
|
|
const configPath = path.join(codexDir, "config.toml");
|
|
const config = `# Codex configuration
|
|
[mcp]
|
|
enabled = true
|
|
`;
|
|
await fs.writeFile(configPath, config);
|
|
|
|
res.json({ success: true, configPath });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Get platform info
|
|
router.get("/platform", async (_req: Request, res: Response) => {
|
|
try {
|
|
const platform = os.platform();
|
|
res.json({
|
|
success: true,
|
|
platform,
|
|
arch: os.arch(),
|
|
homeDir: os.homedir(),
|
|
isWindows: platform === "win32",
|
|
isMac: platform === "darwin",
|
|
isLinux: platform === "linux",
|
|
});
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
// Test OpenAI connection
|
|
router.post("/test-openai", async (req: Request, res: Response) => {
|
|
try {
|
|
const { apiKey } = req.body as { apiKey?: string };
|
|
const key = apiKey || apiKeys.openai || process.env.OPENAI_API_KEY;
|
|
|
|
if (!key) {
|
|
res.json({ success: false, error: "No OpenAI API key provided" });
|
|
return;
|
|
}
|
|
|
|
// Simple test - just verify the key format
|
|
if (!key.startsWith("sk-")) {
|
|
res.json({ success: false, error: "Invalid OpenAI API key format" });
|
|
return;
|
|
}
|
|
|
|
res.json({ success: true, message: "API key format is valid" });
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
res.status(500).json({ success: false, error: message });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|