mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
refactoring the api endpoints to be separate files to reduce context usage
This commit is contained in:
68
apps/server/src/routes/setup/common.ts
Normal file
68
apps/server/src/routes/setup/common.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Common utilities and state for setup routes
|
||||
*/
|
||||
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
|
||||
const logger = createLogger("Setup");
|
||||
|
||||
// Storage for API keys (in-memory cache)
|
||||
export const apiKeys: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Helper to persist API keys to .env file
|
||||
*/
|
||||
export 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"));
|
||||
logger.info(`[Setup] Persisted ${key} to .env file`);
|
||||
} catch (error) {
|
||||
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error message from error object
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : "Unknown error";
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error details consistently
|
||||
*/
|
||||
export function logError(error: unknown, context: string): void {
|
||||
logger.error(`❌ ${context}:`, error);
|
||||
}
|
||||
183
apps/server/src/routes/setup/get-claude-status.ts
Normal file
183
apps/server/src/routes/setup/get-claude-status.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Business logic for getting Claude CLI status
|
||||
*/
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { apiKeys } from "./common.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function getClaudeStatus() {
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
status: installed ? "installed" : "not_installed",
|
||||
installed,
|
||||
method,
|
||||
version,
|
||||
path: cliPath,
|
||||
auth,
|
||||
};
|
||||
}
|
||||
24
apps/server/src/routes/setup/index.ts
Normal file
24
apps/server/src/routes/setup/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createClaudeStatusHandler } from "./routes/claude-status.js";
|
||||
import { createInstallClaudeHandler } from "./routes/install-claude.js";
|
||||
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
|
||||
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
|
||||
import { createApiKeysHandler } from "./routes/api-keys.js";
|
||||
import { createPlatformHandler } from "./routes/platform.js";
|
||||
|
||||
export function createSetupRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/claude-status", createClaudeStatusHandler());
|
||||
router.post("/install-claude", createInstallClaudeHandler());
|
||||
router.post("/auth-claude", createAuthClaudeHandler());
|
||||
router.post("/store-api-key", createStoreApiKeyHandler());
|
||||
router.get("/api-keys", createApiKeysHandler());
|
||||
router.get("/platform", createPlatformHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
21
apps/server/src/routes/setup/routes/api-keys.ts
Normal file
21
apps/server/src/routes/setup/routes/api-keys.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* GET /api-keys endpoint - Get API keys status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { apiKeys, getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createApiKeysHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
|
||||
hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get API keys failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
22
apps/server/src/routes/setup/routes/auth-claude.ts
Normal file
22
apps/server/src/routes/setup/routes/auth-claude.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* POST /auth-claude endpoint - Auth Claude
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createAuthClaudeHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "claude login",
|
||||
message: "Please run 'claude login' in your terminal to authenticate",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Auth Claude failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
22
apps/server/src/routes/setup/routes/claude-status.ts
Normal file
22
apps/server/src/routes/setup/routes/claude-status.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* GET /claude-status endpoint - Get Claude CLI status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getClaudeStatus } from "../get-claude-status.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createClaudeStatusHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = await getClaudeStatus();
|
||||
res.json({
|
||||
success: true,
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get Claude status failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
23
apps/server/src/routes/setup/routes/install-claude.ts
Normal file
23
apps/server/src/routes/setup/routes/install-claude.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* POST /install-claude endpoint - Install Claude CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createInstallClaudeHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
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) {
|
||||
logError(error, "Install Claude CLI failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
27
apps/server/src/routes/setup/routes/platform.ts
Normal file
27
apps/server/src/routes/setup/routes/platform.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* GET /platform endpoint - Get platform info
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import os from "os";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
|
||||
export function createPlatformHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
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) {
|
||||
logError(error, "Get platform info failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
58
apps/server/src/routes/setup/routes/store-api-key.ts
Normal file
58
apps/server/src/routes/setup/routes/store-api-key.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* POST /store-api-key endpoint - Store API key
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import {
|
||||
apiKeys,
|
||||
persistApiKeyToEnv,
|
||||
getErrorMessage,
|
||||
logError,
|
||||
} from "../common.js";
|
||||
import { createLogger } from "../../../lib/logger.js";
|
||||
|
||||
const logger = createLogger("Setup");
|
||||
|
||||
export function createStoreApiKeyHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
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);
|
||||
logger.info("[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);
|
||||
logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY");
|
||||
} else if (provider === "google") {
|
||||
process.env.GOOGLE_API_KEY = apiKey;
|
||||
await persistApiKeyToEnv("GOOGLE_API_KEY", apiKey);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Store API key failed");
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user