mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Improved the CodexCliDetector to provide detailed logging and better error handling when reading the authentication file. - Updated the authentication method determination in the settings and setup views to prioritize CLI-based methods over traditional API key methods. - Expanded the CodexAuthStatus interface to include new authentication methods, ensuring accurate representation of the authentication state. - Enhanced UI feedback in the settings view to reflect the new authentication methods, improving user experience.
676 lines
22 KiB
JavaScript
676 lines
22 KiB
JavaScript
const { execSync, spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
/**
|
|
* Codex CLI Detector - Checks if OpenAI Codex CLI is installed
|
|
*
|
|
* Codex CLI is OpenAI's agent CLI tool that allows users to use
|
|
* GPT-5.1 Codex models (gpt-5.1-codex-max, gpt-5.1-codex, etc.)
|
|
* for code generation and agentic tasks.
|
|
*/
|
|
class CodexCliDetector {
|
|
/**
|
|
* Get the path to Codex config directory
|
|
* @returns {string} Path to .codex directory
|
|
*/
|
|
static getConfigDir() {
|
|
return path.join(os.homedir(), '.codex');
|
|
}
|
|
|
|
/**
|
|
* Get the path to Codex auth file
|
|
* @returns {string} Path to auth.json
|
|
*/
|
|
static getAuthPath() {
|
|
return path.join(this.getConfigDir(), 'auth.json');
|
|
}
|
|
|
|
/**
|
|
* Check Codex authentication status
|
|
* @returns {Object} Authentication status
|
|
*/
|
|
static checkAuth() {
|
|
console.log('[CodexCliDetector] Checking auth status...');
|
|
try {
|
|
const authPath = this.getAuthPath();
|
|
const envApiKey = process.env.OPENAI_API_KEY;
|
|
console.log('[CodexCliDetector] Auth path:', authPath);
|
|
console.log('[CodexCliDetector] Has env API key:', !!envApiKey);
|
|
|
|
// First, try to verify authentication using codex CLI command if available
|
|
try {
|
|
const detection = this.detectCodexInstallation();
|
|
if (detection.installed) {
|
|
try {
|
|
// Use 'codex login status' to verify authentication
|
|
const statusOutput = execSync(`"${detection.path || 'codex'}" login status 2>/dev/null`, {
|
|
encoding: 'utf-8',
|
|
timeout: 5000
|
|
});
|
|
|
|
// If command succeeds and shows logged in status
|
|
if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'cli_verified',
|
|
hasAuthFile: fs.existsSync(authPath),
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (cli_verified):', result);
|
|
return result;
|
|
}
|
|
} catch (statusError) {
|
|
// status command failed, continue with file-based check
|
|
}
|
|
}
|
|
} catch (verifyError) {
|
|
// CLI verification failed, continue with file-based check
|
|
}
|
|
|
|
// Check if auth file exists
|
|
if (fs.existsSync(authPath)) {
|
|
console.log('[CodexCliDetector] Auth file exists, reading content...');
|
|
let auth = null;
|
|
try {
|
|
const content = fs.readFileSync(authPath, 'utf-8');
|
|
auth = JSON.parse(content);
|
|
console.log('[CodexCliDetector] Auth file content keys:', Object.keys(auth));
|
|
console.log('[CodexCliDetector] Auth file has token object:', !!auth.token);
|
|
if (auth.token) {
|
|
console.log('[CodexCliDetector] Token object keys:', Object.keys(auth.token));
|
|
}
|
|
|
|
// Check for token object structure (from codex auth login)
|
|
// Structure: { token: { Id_token, access_token, refresh_token }, last_refresh: ... }
|
|
if (auth.token && typeof auth.token === 'object') {
|
|
const token = auth.token;
|
|
if (token.Id_token || token.access_token || token.refresh_token || token.id_token) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'cli_tokens', // Distinguish token-based auth from API key auth
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (cli_tokens):', result);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Check for tokens at root level (alternative structure)
|
|
if (auth.access_token || auth.refresh_token || auth.Id_token || auth.id_token) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'cli_tokens', // These are tokens, not API keys
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (cli_tokens - root level):', result);
|
|
return result;
|
|
}
|
|
|
|
// Check for various possible API key fields that codex might use
|
|
// Note: access_token is NOT an API key, it's a token, so we check for it above
|
|
if (auth.api_key || auth.openai_api_key || auth.apiKey) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (auth_file - API key):', result);
|
|
return result;
|
|
}
|
|
} catch (error) {
|
|
console.error('[CodexCliDetector] Error reading/parsing auth file:', error.message);
|
|
// If we can't parse the file, we can't determine auth status
|
|
return {
|
|
authenticated: false,
|
|
method: 'none',
|
|
hasAuthFile: false,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
}
|
|
|
|
// Also check if the file has any meaningful content (non-empty object)
|
|
// This is a fallback - but we should still try to detect if it's tokens
|
|
if (!auth) {
|
|
// File exists but couldn't be parsed
|
|
return {
|
|
authenticated: false,
|
|
method: 'none',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
}
|
|
|
|
const keys = Object.keys(auth);
|
|
console.log('[CodexCliDetector] File has content, keys:', keys);
|
|
if (keys.length > 0) {
|
|
// Check again for tokens in case we missed them (maybe nested differently)
|
|
const hasTokens = keys.some(key =>
|
|
key.toLowerCase().includes('token') ||
|
|
key.toLowerCase().includes('refresh') ||
|
|
(auth[key] && typeof auth[key] === 'object' && (
|
|
auth[key].access_token || auth[key].refresh_token || auth[key].Id_token || auth[key].id_token
|
|
))
|
|
);
|
|
|
|
if (hasTokens) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'cli_tokens',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (cli_tokens - fallback detection):', result);
|
|
return result;
|
|
}
|
|
|
|
// File exists and has content, likely authenticated
|
|
// Try to verify by checking if codex command works
|
|
try {
|
|
const detection = this.detectCodexInstallation();
|
|
if (detection.installed) {
|
|
// Try to verify auth by running a simple command
|
|
try {
|
|
execSync(`"${detection.path || 'codex'}" --version 2>/dev/null`, {
|
|
encoding: 'utf-8',
|
|
timeout: 3000
|
|
});
|
|
// If command succeeds, assume authenticated
|
|
// But check if it's likely tokens vs API key based on file structure
|
|
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
|
|
const result = {
|
|
authenticated: true,
|
|
method: likelyTokens ? 'cli_tokens' : 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (verified via CLI, method:', result.method, '):', result);
|
|
return result;
|
|
} catch (cmdError) {
|
|
// Command failed, but file exists - might still be authenticated
|
|
// Check if it's likely tokens
|
|
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
|
|
const result = {
|
|
authenticated: true,
|
|
method: likelyTokens ? 'cli_tokens' : 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (file exists, method:', result.method, '):', result);
|
|
return result;
|
|
}
|
|
}
|
|
} catch (verifyError) {
|
|
// Verification failed, but file exists with content
|
|
// Check if it's likely tokens
|
|
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
|
|
const result = {
|
|
authenticated: true,
|
|
method: likelyTokens ? 'cli_tokens' : 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (fallback, method:', result.method, '):', result);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check environment variable
|
|
if (envApiKey) {
|
|
const result = {
|
|
authenticated: true,
|
|
method: 'env_var',
|
|
hasAuthFile: false,
|
|
hasEnvKey: true,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (env_var):', result);
|
|
return result;
|
|
}
|
|
|
|
// If auth file exists but we didn't find standard keys,
|
|
// check if codex CLI is installed and try to verify auth
|
|
if (fs.existsSync(authPath)) {
|
|
try {
|
|
const detection = this.detectCodexInstallation();
|
|
if (detection.installed) {
|
|
// Auth file exists and CLI is installed - likely authenticated
|
|
// The file existing is a good indicator that login was successful
|
|
return {
|
|
authenticated: true,
|
|
method: 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
}
|
|
} catch (verifyError) {
|
|
// Verification attempt failed, but file exists
|
|
// Assume authenticated if file exists
|
|
return {
|
|
authenticated: true,
|
|
method: 'auth_file',
|
|
hasAuthFile: true,
|
|
hasEnvKey: !!envApiKey,
|
|
authPath
|
|
};
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
authenticated: false,
|
|
method: 'none',
|
|
hasAuthFile: false,
|
|
hasEnvKey: false,
|
|
authPath
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (not authenticated):', result);
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[CodexCliDetector] Error checking auth:', error);
|
|
const result = {
|
|
authenticated: false,
|
|
method: 'none',
|
|
error: error.message
|
|
};
|
|
console.log('[CodexCliDetector] Auth result (error):', result);
|
|
return result;
|
|
}
|
|
}
|
|
/**
|
|
* Check if Codex CLI is installed and accessible
|
|
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'npm'|'brew'|'none' }
|
|
*/
|
|
static detectCodexInstallation() {
|
|
try {
|
|
// Method 1: Check if 'codex' command is in PATH
|
|
try {
|
|
const codexPath = execSync('which codex 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
if (codexPath) {
|
|
const version = this.getCodexVersion(codexPath);
|
|
return {
|
|
installed: true,
|
|
path: codexPath,
|
|
version: version,
|
|
method: 'cli'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
// CLI not in PATH, continue checking other methods
|
|
}
|
|
|
|
// Method 2: Check for npm global installation
|
|
try {
|
|
const npmListOutput = execSync('npm list -g @openai/codex --depth=0 2>/dev/null', { encoding: 'utf-8' });
|
|
if (npmListOutput && npmListOutput.includes('@openai/codex')) {
|
|
// Get the path from npm bin
|
|
const npmBinPath = execSync('npm bin -g', { encoding: 'utf-8' }).trim();
|
|
const codexPath = path.join(npmBinPath, 'codex');
|
|
const version = this.getCodexVersion(codexPath);
|
|
return {
|
|
installed: true,
|
|
path: codexPath,
|
|
version: version,
|
|
method: 'npm'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
// npm global not found
|
|
}
|
|
|
|
// Method 3: Check for Homebrew installation on macOS
|
|
if (process.platform === 'darwin') {
|
|
try {
|
|
const brewList = execSync('brew list --formula 2>/dev/null', { encoding: 'utf-8' });
|
|
if (brewList.includes('codex')) {
|
|
const brewPrefixOutput = execSync('brew --prefix codex 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
const codexPath = path.join(brewPrefixOutput, 'bin', 'codex');
|
|
const version = this.getCodexVersion(codexPath);
|
|
return {
|
|
installed: true,
|
|
path: codexPath,
|
|
version: version,
|
|
method: 'brew'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
// Homebrew not found or codex not installed via brew
|
|
}
|
|
}
|
|
|
|
// Method 4: Check Windows path
|
|
if (process.platform === 'win32') {
|
|
try {
|
|
const codexPath = execSync('where codex 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0];
|
|
if (codexPath) {
|
|
const version = this.getCodexVersion(codexPath);
|
|
return {
|
|
installed: true,
|
|
path: codexPath,
|
|
version: version,
|
|
method: 'cli'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
// Not found on Windows
|
|
}
|
|
}
|
|
|
|
// Method 5: Check common installation paths
|
|
const commonPaths = [
|
|
path.join(os.homedir(), '.local', 'bin', 'codex'),
|
|
path.join(os.homedir(), '.npm-global', 'bin', 'codex'),
|
|
'/usr/local/bin/codex',
|
|
'/opt/homebrew/bin/codex',
|
|
];
|
|
|
|
for (const checkPath of commonPaths) {
|
|
if (fs.existsSync(checkPath)) {
|
|
const version = this.getCodexVersion(checkPath);
|
|
return {
|
|
installed: true,
|
|
path: checkPath,
|
|
version: version,
|
|
method: 'cli'
|
|
};
|
|
}
|
|
}
|
|
|
|
// Method 6: Check if OPENAI_API_KEY is set (can use Codex API directly)
|
|
if (process.env.OPENAI_API_KEY) {
|
|
return {
|
|
installed: false,
|
|
path: null,
|
|
version: null,
|
|
method: 'api-key-only',
|
|
hasApiKey: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
installed: false,
|
|
path: null,
|
|
version: null,
|
|
method: 'none'
|
|
};
|
|
} catch (error) {
|
|
console.error('[CodexCliDetector] Error detecting Codex installation:', error);
|
|
return {
|
|
installed: false,
|
|
path: null,
|
|
version: null,
|
|
method: 'none',
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Codex CLI version from executable path
|
|
* @param {string} codexPath Path to codex executable
|
|
* @returns {string|null} Version string or null
|
|
*/
|
|
static getCodexVersion(codexPath) {
|
|
try {
|
|
const version = execSync(`"${codexPath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
return version || null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get installation info and recommendations
|
|
* @returns {Object} Installation status and recommendations
|
|
*/
|
|
static getInstallationInfo() {
|
|
const detection = this.detectCodexInstallation();
|
|
|
|
if (detection.installed) {
|
|
return {
|
|
status: 'installed',
|
|
method: detection.method,
|
|
version: detection.version,
|
|
path: detection.path,
|
|
recommendation: detection.method === 'cli'
|
|
? 'Using Codex CLI - ready for GPT-5.1 Codex models'
|
|
: `Using Codex CLI via ${detection.method} - ready for GPT-5.1 Codex models`
|
|
};
|
|
}
|
|
|
|
// Not installed but has API key
|
|
if (detection.method === 'api-key-only') {
|
|
return {
|
|
status: 'api_key_only',
|
|
method: 'api-key-only',
|
|
recommendation: 'OPENAI_API_KEY detected but Codex CLI not installed. Install Codex CLI for full agentic capabilities.',
|
|
installCommands: this.getInstallCommands()
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'not_installed',
|
|
recommendation: 'Install OpenAI Codex CLI to use GPT-5.1 Codex models for agentic tasks',
|
|
installCommands: this.getInstallCommands()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get installation commands for different platforms
|
|
* @returns {Object} Installation commands by platform
|
|
*/
|
|
static getInstallCommands() {
|
|
return {
|
|
npm: 'npm install -g @openai/codex@latest',
|
|
macos: 'brew install codex',
|
|
linux: 'npm install -g @openai/codex@latest',
|
|
windows: 'npm install -g @openai/codex@latest'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if Codex CLI supports a specific model
|
|
* @param {string} model Model name to check
|
|
* @returns {boolean} Whether the model is supported
|
|
*/
|
|
static isModelSupported(model) {
|
|
const supportedModels = [
|
|
'gpt-5.1-codex-max',
|
|
'gpt-5.1-codex',
|
|
'gpt-5.1-codex-mini',
|
|
'gpt-5.1'
|
|
];
|
|
return supportedModels.includes(model);
|
|
}
|
|
|
|
/**
|
|
* Get default model for Codex CLI
|
|
* @returns {string} Default model name
|
|
*/
|
|
static getDefaultModel() {
|
|
return 'gpt-5.1-codex-max';
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive installation info including auth status
|
|
* @returns {Object} Full status object
|
|
*/
|
|
static getFullStatus() {
|
|
const installation = this.detectCodexInstallation();
|
|
const auth = this.checkAuth();
|
|
const info = this.getInstallationInfo();
|
|
|
|
return {
|
|
...info,
|
|
auth,
|
|
installation
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Install Codex CLI using npm
|
|
* @param {Function} onProgress Callback for progress updates
|
|
* @returns {Promise<Object>} Installation result
|
|
*/
|
|
static async installCli(onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const command = 'npm';
|
|
const args = ['install', '-g', '@openai/codex@latest'];
|
|
|
|
const proc = spawn(command, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
shell: true
|
|
});
|
|
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
proc.stdout.on('data', (data) => {
|
|
const text = data.toString();
|
|
output += text;
|
|
if (onProgress) {
|
|
onProgress({ type: 'stdout', data: text });
|
|
}
|
|
});
|
|
|
|
proc.stderr.on('data', (data) => {
|
|
const text = data.toString();
|
|
errorOutput += text;
|
|
// npm often outputs progress to stderr
|
|
if (onProgress) {
|
|
onProgress({ type: 'stderr', data: text });
|
|
}
|
|
});
|
|
|
|
proc.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve({
|
|
success: true,
|
|
output,
|
|
message: 'Codex CLI installed successfully'
|
|
});
|
|
} else {
|
|
reject({
|
|
success: false,
|
|
error: errorOutput || `Installation failed with code ${code}`,
|
|
output
|
|
});
|
|
}
|
|
});
|
|
|
|
proc.on('error', (error) => {
|
|
reject({
|
|
success: false,
|
|
error: error.message,
|
|
output
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Authenticate Codex CLI - opens browser for OAuth or stores API key
|
|
* @param {string} apiKey Optional API key to store
|
|
* @param {Function} onProgress Callback for progress updates
|
|
* @returns {Promise<Object>} Authentication result
|
|
*/
|
|
static async authenticate(apiKey, onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const detection = this.detectCodexInstallation();
|
|
|
|
if (!detection.installed) {
|
|
reject({
|
|
success: false,
|
|
error: 'Codex CLI is not installed'
|
|
});
|
|
return;
|
|
}
|
|
|
|
const codexPath = detection.path || 'codex';
|
|
|
|
if (apiKey) {
|
|
// Store API key directly using codex auth command
|
|
const proc = spawn(codexPath, ['auth', 'login', '--api-key', apiKey], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
shell: false
|
|
});
|
|
|
|
let output = '';
|
|
let errorOutput = '';
|
|
|
|
proc.stdout.on('data', (data) => {
|
|
const text = data.toString();
|
|
output += text;
|
|
if (onProgress) {
|
|
onProgress({ type: 'stdout', data: text });
|
|
}
|
|
});
|
|
|
|
proc.stderr.on('data', (data) => {
|
|
const text = data.toString();
|
|
errorOutput += text;
|
|
if (onProgress) {
|
|
onProgress({ type: 'stderr', data: text });
|
|
}
|
|
});
|
|
|
|
proc.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve({
|
|
success: true,
|
|
output,
|
|
message: 'Codex CLI authenticated successfully'
|
|
});
|
|
} else {
|
|
reject({
|
|
success: false,
|
|
error: errorOutput || `Authentication failed with code ${code}`,
|
|
output
|
|
});
|
|
}
|
|
});
|
|
|
|
proc.on('error', (error) => {
|
|
reject({
|
|
success: false,
|
|
error: error.message,
|
|
output
|
|
});
|
|
});
|
|
} else {
|
|
// Require manual authentication
|
|
if (onProgress) {
|
|
onProgress({
|
|
type: 'info',
|
|
data: 'Please run the following command in your terminal to authenticate:\n\ncodex auth login\n\nThen return here to continue setup.'
|
|
});
|
|
}
|
|
|
|
resolve({
|
|
success: true,
|
|
requiresManualAuth: true,
|
|
command: `${codexPath} auth login`,
|
|
message: 'Please authenticate Codex CLI manually'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = CodexCliDetector;
|