mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: fix CLI authentication detection to prevent unnecessary browser prompts
- Fix Claude, Codex, and Cursor auth handlers to check if CLI is already authenticated - Use same detection logic as each provider's internal checkAuth/codexAuthIndicators() - For Codex: Check for API keys and auth files before requiring manual login - For Cursor: Check for env var and credentials files before requiring manual auth - For Claude: Check for cached auth tokens, settings, and credentials files - If CLI is already authenticated: Just reconnect by removing disconnected marker - If CLI needs auth: Tell user to manually run login command - This prevents timeout errors when login commands can't run in non-interactive mode
This commit is contained in:
@@ -8,6 +8,25 @@
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import type { InstallationStatus, ModelDefinition } from './types.js';
|
||||
import { isCursorModel, isCodexModel, type ModelProvider } from '@automaker/types';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DISCONNECTED_MARKERS: Record<string, string> = {
|
||||
claude: '.claude-disconnected',
|
||||
codex: '.codex-disconnected',
|
||||
cursor: '.cursor-disconnected',
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a provider CLI is disconnected from the app
|
||||
*/
|
||||
export function isProviderDisconnected(providerName: string): boolean {
|
||||
const markerFile = DISCONNECTED_MARKERS[providerName.toLowerCase()];
|
||||
if (!markerFile) return false;
|
||||
|
||||
const markerPath = path.join(process.cwd(), '.automaker', markerFile);
|
||||
return fs.existsSync(markerPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider registration entry
|
||||
@@ -75,10 +94,26 @@ export class ProviderFactory {
|
||||
* Get the appropriate provider for a given model ID
|
||||
*
|
||||
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto")
|
||||
* @param options Optional settings
|
||||
* @param options.throwOnDisconnected Throw error if provider is disconnected (default: true)
|
||||
* @returns Provider instance for the model
|
||||
* @throws Error if provider is disconnected and throwOnDisconnected is true
|
||||
*/
|
||||
static getProviderForModel(modelId: string): BaseProvider {
|
||||
const providerName = this.getProviderNameForModel(modelId);
|
||||
static getProviderForModel(
|
||||
modelId: string,
|
||||
options: { throwOnDisconnected?: boolean } = {}
|
||||
): BaseProvider {
|
||||
const { throwOnDisconnected = true } = options;
|
||||
const providerName = this.getProviderForModelName(modelId);
|
||||
|
||||
// Check if provider is disconnected
|
||||
if (throwOnDisconnected && isProviderDisconnected(providerName)) {
|
||||
throw new Error(
|
||||
`${providerName.charAt(0).toUpperCase() + providerName.slice(1)} CLI is disconnected from the app. ` +
|
||||
`Please go to Settings > Providers and click "Sign In" to reconnect.`
|
||||
);
|
||||
}
|
||||
|
||||
const provider = this.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
@@ -93,6 +128,35 @@ export class ProviderFactory {
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider name for a given model ID (without creating provider instance)
|
||||
*/
|
||||
static getProviderForModelName(modelId: string): string {
|
||||
const lowerModel = modelId.toLowerCase();
|
||||
|
||||
// Get all registered providers sorted by priority (descending)
|
||||
const registrations = Array.from(providerRegistry.entries()).sort(
|
||||
([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0)
|
||||
);
|
||||
|
||||
// Check each provider's canHandleModel function
|
||||
for (const [name, reg] of registrations) {
|
||||
if (reg.canHandleModel?.(lowerModel)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Check for explicit prefixes
|
||||
for (const [name] of registrations) {
|
||||
if (lowerModel.startsWith(`${name}-`)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to claude (first registered provider or claude)
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available providers
|
||||
*/
|
||||
|
||||
@@ -6,9 +6,24 @@ import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getClaudeCliPaths, getClaudeAuthIndicators, systemPathAccess } from '@automaker/platform';
|
||||
import { getApiKey } from './common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const DISCONNECTED_MARKER_FILE = '.claude-disconnected';
|
||||
|
||||
function isDisconnectedFromApp(): boolean {
|
||||
try {
|
||||
// Check if we're in a project directory
|
||||
const projectRoot = process.cwd();
|
||||
const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE);
|
||||
return fs.existsSync(markerPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getClaudeStatus() {
|
||||
let installed = false;
|
||||
let version = '';
|
||||
@@ -60,6 +75,30 @@ export async function getClaudeStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has manually disconnected from the app
|
||||
if (isDisconnectedFromApp()) {
|
||||
return {
|
||||
status: installed ? 'installed' : 'not_installed',
|
||||
installed,
|
||||
method,
|
||||
version,
|
||||
path: cliPath,
|
||||
auth: {
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
hasCredentialsFile: false,
|
||||
hasToken: false,
|
||||
hasStoredOAuthToken: false,
|
||||
hasStoredApiKey: false,
|
||||
hasEnvApiKey: false,
|
||||
oauthTokenValid: false,
|
||||
apiKeyValid: false,
|
||||
hasCliAuth: false,
|
||||
hasRecentActivity: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -17,6 +17,10 @@ import { createCursorStatusHandler } from './routes/cursor-status.js';
|
||||
import { createCodexStatusHandler } from './routes/codex-status.js';
|
||||
import { createInstallCodexHandler } from './routes/install-codex.js';
|
||||
import { createAuthCodexHandler } from './routes/auth-codex.js';
|
||||
import { createAuthCursorHandler } from './routes/auth-cursor.js';
|
||||
import { createDeauthClaudeHandler } from './routes/deauth-claude.js';
|
||||
import { createDeauthCodexHandler } from './routes/deauth-codex.js';
|
||||
import { createDeauthCursorHandler } from './routes/deauth-cursor.js';
|
||||
import {
|
||||
createGetCursorConfigHandler,
|
||||
createSetCursorDefaultModelHandler,
|
||||
@@ -34,6 +38,7 @@ export function createSetupRoutes(): Router {
|
||||
router.get('/claude-status', createClaudeStatusHandler());
|
||||
router.post('/install-claude', createInstallClaudeHandler());
|
||||
router.post('/auth-claude', createAuthClaudeHandler());
|
||||
router.post('/deauth-claude', createDeauthClaudeHandler());
|
||||
router.post('/store-api-key', createStoreApiKeyHandler());
|
||||
router.post('/delete-api-key', createDeleteApiKeyHandler());
|
||||
router.get('/api-keys', createApiKeysHandler());
|
||||
@@ -44,11 +49,14 @@ export function createSetupRoutes(): Router {
|
||||
|
||||
// Cursor CLI routes
|
||||
router.get('/cursor-status', createCursorStatusHandler());
|
||||
router.post('/auth-cursor', createAuthCursorHandler());
|
||||
router.post('/deauth-cursor', createDeauthCursorHandler());
|
||||
|
||||
// Codex CLI routes
|
||||
router.get('/codex-status', createCodexStatusHandler());
|
||||
router.post('/install-codex', createInstallCodexHandler());
|
||||
router.post('/auth-codex', createAuthCodexHandler());
|
||||
router.post('/deauth-codex', createDeauthCodexHandler());
|
||||
router.get('/cursor-config', createGetCursorConfigHandler());
|
||||
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
|
||||
router.post('/cursor-config/models', createSetCursorModelsHandler());
|
||||
|
||||
@@ -4,19 +4,54 @@
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
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",
|
||||
});
|
||||
// Remove the disconnected marker file to reconnect the app to the CLI
|
||||
const markerPath = path.join(process.cwd(), '.automaker', '.claude-disconnected');
|
||||
if (fs.existsSync(markerPath)) {
|
||||
fs.unlinkSync(markerPath);
|
||||
}
|
||||
|
||||
// Check if CLI is already authenticated by checking auth indicators
|
||||
const { getClaudeAuthIndicators } = await import('@automaker/platform');
|
||||
const indicators = await getClaudeAuthIndicators();
|
||||
const isAlreadyAuthenticated =
|
||||
indicators.hasStatsCacheWithActivity ||
|
||||
(indicators.hasSettingsFile && indicators.hasProjectsSessions) ||
|
||||
indicators.hasCredentialsFile;
|
||||
|
||||
if (isAlreadyAuthenticated) {
|
||||
// CLI is already authenticated, just reconnect
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Claude CLI is now linked with the app',
|
||||
wasAlreadyAuthenticated: true,
|
||||
});
|
||||
} else {
|
||||
// CLI needs authentication - but we can't run claude login here
|
||||
// because it requires browser OAuth. Just reconnect and let the user authenticate if needed.
|
||||
res.json({
|
||||
success: true,
|
||||
message:
|
||||
'Claude CLI is now linked with the app. If prompted, please authenticate with "claude login" in your terminal.',
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Auth Claude failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to link Claude CLI with the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,27 +4,46 @@
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { logError, getErrorMessage } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Creates handler for POST /api/setup/auth-codex
|
||||
* Returns instructions for manual Codex CLI authentication
|
||||
*/
|
||||
export function createAuthCodexHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const loginCommand = 'codex login';
|
||||
// Remove the disconnected marker file to reconnect the app to the CLI
|
||||
const markerPath = path.join(process.cwd(), '.automaker', '.codex-disconnected');
|
||||
if (fs.existsSync(markerPath)) {
|
||||
fs.unlinkSync(markerPath);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: loginCommand,
|
||||
message: `Please authenticate Codex CLI manually by running: ${loginCommand}`,
|
||||
});
|
||||
// Use the same detection logic as the Codex provider
|
||||
const { getCodexAuthIndicators } = await import('@automaker/platform');
|
||||
const indicators = await getCodexAuthIndicators();
|
||||
|
||||
const isAlreadyAuthenticated =
|
||||
indicators.hasApiKey || indicators.hasAuthFile || indicators.hasOAuthToken;
|
||||
|
||||
if (isAlreadyAuthenticated) {
|
||||
// Already has authentication, just reconnect
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Codex CLI is now linked with the app',
|
||||
wasAlreadyAuthenticated: true,
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message:
|
||||
'Codex CLI is now linked with the app. If prompted, please authenticate with "codex login" in your terminal.',
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Auth Codex failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to link Codex CLI with the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
73
apps/server/src/routes/setup/routes/auth-cursor.ts
Normal file
73
apps/server/src/routes/setup/routes/auth-cursor.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* POST /auth-cursor endpoint - Authenticate Cursor CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { logError, getErrorMessage } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
export function createAuthCursorHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Remove the disconnected marker file to reconnect the app to the CLI
|
||||
const markerPath = path.join(process.cwd(), '.automaker', '.cursor-disconnected');
|
||||
if (fs.existsSync(markerPath)) {
|
||||
fs.unlinkSync(markerPath);
|
||||
}
|
||||
|
||||
// Check if Cursor is already authenticated using the same logic as CursorProvider
|
||||
const isAlreadyAuthenticated = (): boolean => {
|
||||
// Check for API key in environment
|
||||
if (process.env.CURSOR_API_KEY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for credentials files
|
||||
const credentialPaths = [
|
||||
path.join(os.homedir(), '.cursor', 'credentials.json'),
|
||||
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
|
||||
];
|
||||
|
||||
for (const credPath of credentialPaths) {
|
||||
if (fs.existsSync(credPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(credPath, 'utf8');
|
||||
const creds = JSON.parse(content);
|
||||
if (creds.accessToken || creds.token) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Invalid credentials file, continue checking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isAlreadyAuthenticated()) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cursor CLI is now linked with the app',
|
||||
wasAlreadyAuthenticated: true,
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message:
|
||||
'Cursor CLI is now linked with the app. If prompted, please authenticate with "cursor auth" in your terminal.',
|
||||
requiresManualAuth: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, 'Auth Cursor failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to link Cursor CLI with the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,20 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { CodexProvider } from '../../../providers/codex-provider.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DISCONNECTED_MARKER_FILE = '.codex-disconnected';
|
||||
|
||||
function isCodexDisconnectedFromApp(): boolean {
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE);
|
||||
return fs.existsSync(markerPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates handler for GET /api/setup/codex-status
|
||||
@@ -16,6 +30,24 @@ export function createCodexStatusHandler() {
|
||||
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Check if user has manually disconnected from the app
|
||||
if (isCodexDisconnectedFromApp()) {
|
||||
res.json({
|
||||
success: true,
|
||||
installed: true,
|
||||
version: null,
|
||||
path: null,
|
||||
auth: {
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
hasApiKey: false,
|
||||
},
|
||||
installCommand,
|
||||
loginCommand,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = new CodexProvider();
|
||||
const status = await provider.detectInstallation();
|
||||
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { CursorProvider } from '../../../providers/cursor-provider.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const DISCONNECTED_MARKER_FILE = '.cursor-disconnected';
|
||||
|
||||
function isCursorDisconnectedFromApp(): boolean {
|
||||
try {
|
||||
const projectRoot = process.cwd();
|
||||
const markerPath = path.join(projectRoot, '.automaker', DISCONNECTED_MARKER_FILE);
|
||||
return fs.existsSync(markerPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates handler for GET /api/setup/cursor-status
|
||||
@@ -16,6 +30,30 @@ export function createCursorStatusHandler() {
|
||||
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Check if user has manually disconnected from the app
|
||||
if (isCursorDisconnectedFromApp()) {
|
||||
const provider = new CursorProvider();
|
||||
const [installed, version] = await Promise.all([
|
||||
provider.isInstalled(),
|
||||
provider.getVersion(),
|
||||
]);
|
||||
const cliPath = installed ? provider.getCliPath() : null;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
installed,
|
||||
version: version || null,
|
||||
path: cliPath,
|
||||
auth: {
|
||||
authenticated: false,
|
||||
method: 'none',
|
||||
},
|
||||
installCommand,
|
||||
loginCommand,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const [installed, version, auth] = await Promise.all([
|
||||
|
||||
44
apps/server/src/routes/setup/routes/deauth-claude.ts
Normal file
44
apps/server/src/routes/setup/routes/deauth-claude.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* POST /deauth-claude endpoint - Sign out from Claude CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function createDeauthClaudeHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Create a marker file to indicate the CLI is disconnected from the app
|
||||
const automakerDir = path.join(process.cwd(), '.automaker');
|
||||
const markerPath = path.join(automakerDir, '.claude-disconnected');
|
||||
|
||||
// Ensure .automaker directory exists
|
||||
if (!fs.existsSync(automakerDir)) {
|
||||
fs.mkdirSync(automakerDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create the marker file with timestamp
|
||||
fs.writeFileSync(
|
||||
markerPath,
|
||||
JSON.stringify({
|
||||
disconnectedAt: new Date().toISOString(),
|
||||
message: 'Claude CLI is disconnected from the app',
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Claude CLI is now disconnected from the app',
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Deauth Claude failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to disconnect Claude CLI from the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
44
apps/server/src/routes/setup/routes/deauth-codex.ts
Normal file
44
apps/server/src/routes/setup/routes/deauth-codex.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* POST /deauth-codex endpoint - Sign out from Codex CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { logError, getErrorMessage } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function createDeauthCodexHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Create a marker file to indicate the CLI is disconnected from the app
|
||||
const automakerDir = path.join(process.cwd(), '.automaker');
|
||||
const markerPath = path.join(automakerDir, '.codex-disconnected');
|
||||
|
||||
// Ensure .automaker directory exists
|
||||
if (!fs.existsSync(automakerDir)) {
|
||||
fs.mkdirSync(automakerDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create the marker file with timestamp
|
||||
fs.writeFileSync(
|
||||
markerPath,
|
||||
JSON.stringify({
|
||||
disconnectedAt: new Date().toISOString(),
|
||||
message: 'Codex CLI is disconnected from the app',
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Codex CLI is now disconnected from the app',
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Deauth Codex failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to disconnect Codex CLI from the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
44
apps/server/src/routes/setup/routes/deauth-cursor.ts
Normal file
44
apps/server/src/routes/setup/routes/deauth-cursor.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* POST /deauth-cursor endpoint - Sign out from Cursor CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { logError, getErrorMessage } from '../common.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function createDeauthCursorHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Create a marker file to indicate the CLI is disconnected from the app
|
||||
const automakerDir = path.join(process.cwd(), '.automaker');
|
||||
const markerPath = path.join(automakerDir, '.cursor-disconnected');
|
||||
|
||||
// Ensure .automaker directory exists
|
||||
if (!fs.existsSync(automakerDir)) {
|
||||
fs.mkdirSync(automakerDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create the marker file with timestamp
|
||||
fs.writeFileSync(
|
||||
markerPath,
|
||||
JSON.stringify({
|
||||
disconnectedAt: new Date().toISOString(),
|
||||
message: 'Cursor CLI is disconnected from the app',
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cursor CLI is now disconnected from the app',
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Deauth Cursor failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
message: 'Failed to disconnect Cursor CLI from the app',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CliStatus } from '../shared/types';
|
||||
import type { ClaudeAuthStatus } from '@/store/setup-store';
|
||||
import { AnthropicIcon } from '@/components/ui/provider-icon';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CliStatusProps {
|
||||
status: CliStatus | null;
|
||||
@@ -81,6 +84,60 @@ function ClaudeCliStatusSkeleton() {
|
||||
}
|
||||
|
||||
export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
|
||||
|
||||
const handleSignIn = useCallback(async () => {
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.authClaude();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed In', {
|
||||
description: 'Successfully authenticated Claude CLI',
|
||||
});
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
const handleSignOut = useCallback(async () => {
|
||||
setIsDeauthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.deauthClaude();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed Out', {
|
||||
description: 'Successfully signed out from Claude CLI',
|
||||
});
|
||||
// Refresh status after successful logout
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsDeauthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
if (!status) return <ClaudeCliStatusSkeleton />;
|
||||
|
||||
return (
|
||||
@@ -153,7 +210,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
||||
</div>
|
||||
{/* Authentication Status */}
|
||||
{authStatus?.authenticated ? (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
@@ -165,6 +222,15 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
||||
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignOut}
|
||||
disabled={isDeauthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -175,9 +241,17 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
Run <code className="font-mono bg-amber-500/10 px-1 rounded">claude login</code>{' '}
|
||||
or set an API key to authenticate.
|
||||
Click Sign In below to get authentication instructions.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignIn}
|
||||
disabled={isAuthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CliStatus } from '../shared/types';
|
||||
import type { CodexAuthStatus } from '@/store/setup-store';
|
||||
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CliStatusProps {
|
||||
status: CliStatus | null;
|
||||
@@ -76,6 +79,60 @@ function CodexCliStatusSkeleton() {
|
||||
}
|
||||
|
||||
export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
|
||||
|
||||
const handleSignIn = useCallback(async () => {
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.authCodex();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed In', {
|
||||
description: 'Successfully authenticated Codex CLI',
|
||||
});
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
const handleSignOut = useCallback(async () => {
|
||||
setIsDeauthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.deauthCodex();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed Out', {
|
||||
description: 'Successfully signed out from Codex CLI',
|
||||
});
|
||||
// Refresh status after successful logout
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsDeauthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
if (!status) return <CodexCliStatusSkeleton />;
|
||||
|
||||
return (
|
||||
@@ -145,7 +202,7 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
||||
</div>
|
||||
{/* Authentication Status */}
|
||||
{authStatus?.authenticated ? (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
@@ -157,6 +214,15 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
||||
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignOut}
|
||||
disabled={isDeauthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -167,9 +233,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
Run <code className="font-mono bg-amber-500/10 px-1 rounded">codex login</code>{' '}
|
||||
or set an API key to authenticate.
|
||||
Click Sign In below to get authentication instructions.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignIn}
|
||||
disabled={isAuthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CursorIcon } from '@/components/ui/provider-icon';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CursorStatus {
|
||||
installed: boolean;
|
||||
@@ -201,6 +204,60 @@ export function ModelConfigSkeleton() {
|
||||
}
|
||||
|
||||
export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStatusProps) {
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
||||
const [isDeauthenticating, setIsDeauthenticating] = useState(false);
|
||||
|
||||
const handleSignIn = useCallback(async () => {
|
||||
setIsAuthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.authCursor();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed In', {
|
||||
description: 'Successfully authenticated Cursor CLI',
|
||||
});
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Authentication Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
const handleSignOut = useCallback(async () => {
|
||||
setIsDeauthenticating(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.setup.deauthCursor();
|
||||
|
||||
if (result.success) {
|
||||
toast.success('Signed Out', {
|
||||
description: 'Successfully signed out from Cursor CLI',
|
||||
});
|
||||
// Refresh status after successful logout
|
||||
onRefresh();
|
||||
} else if (result.error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Sign Out Failed', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
} finally {
|
||||
setIsDeauthenticating(false);
|
||||
}
|
||||
}, [onRefresh]);
|
||||
|
||||
if (!status) return <CursorCliStatusSkeleton />;
|
||||
|
||||
return (
|
||||
@@ -262,7 +319,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
||||
|
||||
{/* Authentication Status */}
|
||||
{status.authenticated ? (
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
@@ -276,6 +333,15 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignOut}
|
||||
disabled={isDeauthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isDeauthenticating ? 'Signing Out...' : 'Sign Out'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -286,9 +352,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||
<p className="text-xs text-amber-400/70 mt-1">
|
||||
Run <code className="font-mono bg-amber-500/10 px-1 rounded">cursor auth</code>{' '}
|
||||
to authenticate with Cursor.
|
||||
Click Sign In below to get authentication instructions.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSignIn}
|
||||
disabled={isAuthenticating}
|
||||
className="mt-3 h-8 text-xs"
|
||||
>
|
||||
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Type definitions for Electron IPC API
|
||||
import type { SessionListItem, Message } from '@/types/electron';
|
||||
import type { ClaudeUsageResponse } from '@/store/app-store';
|
||||
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||
import type {
|
||||
IssueValidationVerdict,
|
||||
IssueValidationConfidence,
|
||||
@@ -725,6 +725,9 @@ export interface ElectronAPI {
|
||||
}>;
|
||||
};
|
||||
ideation?: IdeationAPI;
|
||||
codex?: {
|
||||
getUsage: () => Promise<CodexUsageResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
// Note: Window interface is declared in @/types/electron.d.ts
|
||||
|
||||
@@ -1073,6 +1073,14 @@ export class HttpApiClient implements ElectronAPI {
|
||||
output?: string;
|
||||
}> => this.post('/api/setup/auth-claude'),
|
||||
|
||||
deauthClaude: (): Promise<{
|
||||
success: boolean;
|
||||
requiresManualDeauth?: boolean;
|
||||
command?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> => this.post('/api/setup/deauth-claude'),
|
||||
|
||||
storeApiKey: (
|
||||
provider: string,
|
||||
apiKey: string
|
||||
@@ -1139,6 +1147,24 @@ export class HttpApiClient implements ElectronAPI {
|
||||
error?: string;
|
||||
}> => this.get('/api/setup/cursor-status'),
|
||||
|
||||
authCursor: (): Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
requiresManualAuth?: boolean;
|
||||
terminalOpened?: boolean;
|
||||
command?: string;
|
||||
message?: string;
|
||||
output?: string;
|
||||
}> => this.post('/api/setup/auth-cursor'),
|
||||
|
||||
deauthCursor: (): Promise<{
|
||||
success: boolean;
|
||||
requiresManualDeauth?: boolean;
|
||||
command?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> => this.post('/api/setup/deauth-cursor'),
|
||||
|
||||
getCursorConfig: (
|
||||
projectPath: string
|
||||
): Promise<{
|
||||
@@ -1281,6 +1307,14 @@ export class HttpApiClient implements ElectronAPI {
|
||||
output?: string;
|
||||
}> => this.post('/api/setup/auth-codex'),
|
||||
|
||||
deauthCodex: (): Promise<{
|
||||
success: boolean;
|
||||
requiresManualDeauth?: boolean;
|
||||
command?: string;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}> => this.post('/api/setup/deauth-codex'),
|
||||
|
||||
verifyCodexAuth: (
|
||||
authMethod: 'cli' | 'api_key',
|
||||
apiKey?: string
|
||||
|
||||
12
apps/ui/src/types/electron.d.ts
vendored
12
apps/ui/src/types/electron.d.ts
vendored
@@ -2,6 +2,8 @@
|
||||
* Electron API type definitions
|
||||
*/
|
||||
|
||||
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||
|
||||
export interface ImageAttachment {
|
||||
id?: string; // Optional - may not be present in messages loaded from server
|
||||
data: string; // base64 encoded image data
|
||||
@@ -584,6 +586,16 @@ export interface ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Claude Usage API
|
||||
claude: {
|
||||
getUsage: () => Promise<ClaudeUsageResponse>;
|
||||
};
|
||||
|
||||
// Codex Usage API
|
||||
codex: {
|
||||
getUsage: () => Promise<CodexUsageResponse>;
|
||||
};
|
||||
|
||||
// Worktree Management APIs
|
||||
worktree: WorktreeAPI;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user