mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 21:43:07 +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 { BaseProvider } from './base-provider.js';
|
||||||
import type { InstallationStatus, ModelDefinition } from './types.js';
|
import type { InstallationStatus, ModelDefinition } from './types.js';
|
||||||
import { isCursorModel, isCodexModel, type ModelProvider } from '@automaker/types';
|
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
|
* Provider registration entry
|
||||||
@@ -75,10 +94,26 @@ export class ProviderFactory {
|
|||||||
* Get the appropriate provider for a given model ID
|
* 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 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
|
* @returns Provider instance for the model
|
||||||
|
* @throws Error if provider is disconnected and throwOnDisconnected is true
|
||||||
*/
|
*/
|
||||||
static getProviderForModel(modelId: string): BaseProvider {
|
static getProviderForModel(
|
||||||
const providerName = this.getProviderNameForModel(modelId);
|
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);
|
const provider = this.getProviderByName(providerName);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
@@ -93,6 +128,35 @@ export class ProviderFactory {
|
|||||||
return provider;
|
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
|
* Get all available providers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,9 +6,24 @@ import { exec } from 'child_process';
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { getClaudeCliPaths, getClaudeAuthIndicators, systemPathAccess } from '@automaker/platform';
|
import { getClaudeCliPaths, getClaudeAuthIndicators, systemPathAccess } from '@automaker/platform';
|
||||||
import { getApiKey } from './common.js';
|
import { getApiKey } from './common.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
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() {
|
export async function getClaudeStatus() {
|
||||||
let installed = false;
|
let installed = false;
|
||||||
let version = '';
|
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
|
// Check authentication - detect all possible auth methods
|
||||||
// Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth
|
// Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth
|
||||||
// apiKeys.anthropic stores direct API keys for pay-per-use
|
// 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 { createCodexStatusHandler } from './routes/codex-status.js';
|
||||||
import { createInstallCodexHandler } from './routes/install-codex.js';
|
import { createInstallCodexHandler } from './routes/install-codex.js';
|
||||||
import { createAuthCodexHandler } from './routes/auth-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 {
|
import {
|
||||||
createGetCursorConfigHandler,
|
createGetCursorConfigHandler,
|
||||||
createSetCursorDefaultModelHandler,
|
createSetCursorDefaultModelHandler,
|
||||||
@@ -34,6 +38,7 @@ export function createSetupRoutes(): Router {
|
|||||||
router.get('/claude-status', createClaudeStatusHandler());
|
router.get('/claude-status', createClaudeStatusHandler());
|
||||||
router.post('/install-claude', createInstallClaudeHandler());
|
router.post('/install-claude', createInstallClaudeHandler());
|
||||||
router.post('/auth-claude', createAuthClaudeHandler());
|
router.post('/auth-claude', createAuthClaudeHandler());
|
||||||
|
router.post('/deauth-claude', createDeauthClaudeHandler());
|
||||||
router.post('/store-api-key', createStoreApiKeyHandler());
|
router.post('/store-api-key', createStoreApiKeyHandler());
|
||||||
router.post('/delete-api-key', createDeleteApiKeyHandler());
|
router.post('/delete-api-key', createDeleteApiKeyHandler());
|
||||||
router.get('/api-keys', createApiKeysHandler());
|
router.get('/api-keys', createApiKeysHandler());
|
||||||
@@ -44,11 +49,14 @@ export function createSetupRoutes(): Router {
|
|||||||
|
|
||||||
// Cursor CLI routes
|
// Cursor CLI routes
|
||||||
router.get('/cursor-status', createCursorStatusHandler());
|
router.get('/cursor-status', createCursorStatusHandler());
|
||||||
|
router.post('/auth-cursor', createAuthCursorHandler());
|
||||||
|
router.post('/deauth-cursor', createDeauthCursorHandler());
|
||||||
|
|
||||||
// Codex CLI routes
|
// Codex CLI routes
|
||||||
router.get('/codex-status', createCodexStatusHandler());
|
router.get('/codex-status', createCodexStatusHandler());
|
||||||
router.post('/install-codex', createInstallCodexHandler());
|
router.post('/install-codex', createInstallCodexHandler());
|
||||||
router.post('/auth-codex', createAuthCodexHandler());
|
router.post('/auth-codex', createAuthCodexHandler());
|
||||||
|
router.post('/deauth-codex', createDeauthCodexHandler());
|
||||||
router.get('/cursor-config', createGetCursorConfigHandler());
|
router.get('/cursor-config', createGetCursorConfigHandler());
|
||||||
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
|
router.post('/cursor-config/default-model', createSetCursorDefaultModelHandler());
|
||||||
router.post('/cursor-config/models', createSetCursorModelsHandler());
|
router.post('/cursor-config/models', createSetCursorModelsHandler());
|
||||||
|
|||||||
@@ -4,19 +4,54 @@
|
|||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
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() {
|
export function createAuthClaudeHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
// 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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
requiresManualAuth: true,
|
message: 'Claude CLI is now linked with the app',
|
||||||
command: 'claude login',
|
wasAlreadyAuthenticated: true,
|
||||||
message: "Please run 'claude login' in your terminal to authenticate",
|
|
||||||
});
|
});
|
||||||
|
} 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) {
|
} catch (error) {
|
||||||
logError(error, 'Auth Claude failed');
|
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 type { Request, Response } from 'express';
|
||||||
import { logError, getErrorMessage } from '../common.js';
|
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() {
|
export function createAuthCodexHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
requiresManualAuth: true,
|
message: 'Codex CLI is now linked with the app',
|
||||||
command: loginCommand,
|
wasAlreadyAuthenticated: true,
|
||||||
message: `Please authenticate Codex CLI manually by running: ${loginCommand}`,
|
|
||||||
});
|
});
|
||||||
|
} 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) {
|
} catch (error) {
|
||||||
logError(error, 'Auth Codex failed');
|
logError(error, 'Auth Codex failed');
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: getErrorMessage(error),
|
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 type { Request, Response } from 'express';
|
||||||
import { CodexProvider } from '../../../providers/codex-provider.js';
|
import { CodexProvider } from '../../../providers/codex-provider.js';
|
||||||
import { getErrorMessage, logError } from '../common.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
|
* Creates handler for GET /api/setup/codex-status
|
||||||
@@ -16,6 +30,24 @@ export function createCodexStatusHandler() {
|
|||||||
|
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
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 provider = new CodexProvider();
|
||||||
const status = await provider.detectInstallation();
|
const status = await provider.detectInstallation();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,20 @@
|
|||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { CursorProvider } from '../../../providers/cursor-provider.js';
|
import { CursorProvider } from '../../../providers/cursor-provider.js';
|
||||||
import { getErrorMessage, logError } from '../common.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
|
* Creates handler for GET /api/setup/cursor-status
|
||||||
@@ -16,6 +30,30 @@ export function createCursorStatusHandler() {
|
|||||||
|
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
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 provider = new CursorProvider();
|
||||||
|
|
||||||
const [installed, version, auth] = await Promise.all([
|
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 { Button } from '@/components/ui/button';
|
||||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { CliStatus } from '../shared/types';
|
import type { CliStatus } from '../shared/types';
|
||||||
import type { ClaudeAuthStatus } from '@/store/setup-store';
|
import type { ClaudeAuthStatus } from '@/store/setup-store';
|
||||||
import { AnthropicIcon } from '@/components/ui/provider-icon';
|
import { AnthropicIcon } from '@/components/ui/provider-icon';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface CliStatusProps {
|
interface CliStatusProps {
|
||||||
status: CliStatus | null;
|
status: CliStatus | null;
|
||||||
@@ -81,6 +84,60 @@ function ClaudeCliStatusSkeleton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
|
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 />;
|
if (!status) return <ClaudeCliStatusSkeleton />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -153,7 +210,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
|||||||
</div>
|
</div>
|
||||||
{/* Authentication Status */}
|
{/* Authentication Status */}
|
||||||
{authStatus?.authenticated ? (
|
{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">
|
<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" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -165,6 +222,15 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
|||||||
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,9 +241,17 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
<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>{' '}
|
Click Sign In below to get authentication instructions.
|
||||||
or set an API key to authenticate.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSignIn}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="mt-3 h-8 text-xs"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { CliStatus } from '../shared/types';
|
import type { CliStatus } from '../shared/types';
|
||||||
import type { CodexAuthStatus } from '@/store/setup-store';
|
import type { CodexAuthStatus } from '@/store/setup-store';
|
||||||
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
import { OpenAIIcon } from '@/components/ui/provider-icon';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface CliStatusProps {
|
interface CliStatusProps {
|
||||||
status: CliStatus | null;
|
status: CliStatus | null;
|
||||||
@@ -76,6 +79,60 @@ function CodexCliStatusSkeleton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
|
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 />;
|
if (!status) return <CodexCliStatusSkeleton />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -145,7 +202,7 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
|||||||
</div>
|
</div>
|
||||||
{/* Authentication Status */}
|
{/* Authentication Status */}
|
||||||
{authStatus?.authenticated ? (
|
{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">
|
<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" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -157,6 +214,15 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
|||||||
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -167,9 +233,17 @@ export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: Cl
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
<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>{' '}
|
Click Sign In below to get authentication instructions.
|
||||||
or set an API key to authenticate.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSignIn}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="mt-3 h-8 text-xs"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { CursorIcon } from '@/components/ui/provider-icon';
|
import { CursorIcon } from '@/components/ui/provider-icon';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface CursorStatus {
|
interface CursorStatus {
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
@@ -201,6 +204,60 @@ export function ModelConfigSkeleton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStatusProps) {
|
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 />;
|
if (!status) return <CursorCliStatusSkeleton />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +319,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
|||||||
|
|
||||||
{/* Authentication Status */}
|
{/* Authentication Status */}
|
||||||
{status.authenticated ? (
|
{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">
|
<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" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -276,6 +333,15 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -286,9 +352,17 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
|
||||||
<p className="text-xs text-amber-400/70 mt-1">
|
<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>{' '}
|
Click Sign In below to get authentication instructions.
|
||||||
to authenticate with Cursor.
|
|
||||||
</p>
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSignIn}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="mt-3 h-8 text-xs"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? 'Requesting...' : 'Sign In'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Type definitions for Electron IPC API
|
// Type definitions for Electron IPC API
|
||||||
import type { SessionListItem, Message } from '@/types/electron';
|
import type { SessionListItem, Message } from '@/types/electron';
|
||||||
import type { ClaudeUsageResponse } from '@/store/app-store';
|
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||||
import type {
|
import type {
|
||||||
IssueValidationVerdict,
|
IssueValidationVerdict,
|
||||||
IssueValidationConfidence,
|
IssueValidationConfidence,
|
||||||
@@ -725,6 +725,9 @@ export interface ElectronAPI {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
ideation?: IdeationAPI;
|
ideation?: IdeationAPI;
|
||||||
|
codex?: {
|
||||||
|
getUsage: () => Promise<CodexUsageResponse>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Window interface is declared in @/types/electron.d.ts
|
// Note: Window interface is declared in @/types/electron.d.ts
|
||||||
|
|||||||
@@ -1073,6 +1073,14 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
output?: string;
|
output?: string;
|
||||||
}> => this.post('/api/setup/auth-claude'),
|
}> => 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: (
|
storeApiKey: (
|
||||||
provider: string,
|
provider: string,
|
||||||
apiKey: string
|
apiKey: string
|
||||||
@@ -1139,6 +1147,24 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> => this.get('/api/setup/cursor-status'),
|
}> => 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: (
|
getCursorConfig: (
|
||||||
projectPath: string
|
projectPath: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -1281,6 +1307,14 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
output?: string;
|
output?: string;
|
||||||
}> => this.post('/api/setup/auth-codex'),
|
}> => 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: (
|
verifyCodexAuth: (
|
||||||
authMethod: 'cli' | 'api_key',
|
authMethod: 'cli' | 'api_key',
|
||||||
apiKey?: string
|
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
|
* Electron API type definitions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ClaudeUsageResponse, CodexUsageResponse } from '@/store/app-store';
|
||||||
|
|
||||||
export interface ImageAttachment {
|
export interface ImageAttachment {
|
||||||
id?: string; // Optional - may not be present in messages loaded from server
|
id?: string; // Optional - may not be present in messages loaded from server
|
||||||
data: string; // base64 encoded image data
|
data: string; // base64 encoded image data
|
||||||
@@ -584,6 +586,16 @@ export interface ElectronAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// Claude Usage API
|
||||||
|
claude: {
|
||||||
|
getUsage: () => Promise<ClaudeUsageResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Codex Usage API
|
||||||
|
codex: {
|
||||||
|
getUsage: () => Promise<CodexUsageResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
// Worktree Management APIs
|
// Worktree Management APIs
|
||||||
worktree: WorktreeAPI;
|
worktree: WorktreeAPI;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user