feat(setup): implement setup wizard for CLI tools configuration

- Added a new SetupView component to guide users through the installation and authentication of Claude and Codex CLIs.
- Integrated IPC handlers for checking CLI status, installing, and authenticating both CLIs.
- Enhanced the app store to manage setup state, including first run detection and progress tracking.
- Updated the main application view to redirect to the setup wizard on first run.
- Improved user experience by providing clear instructions and feedback during the setup process.

These changes streamline the initial configuration of CLI tools, ensuring users can easily set up their development environment.
This commit is contained in:
Kacper
2025-12-10 19:15:29 +01:00
parent 2afb5ced90
commit 3bd28d3084
12 changed files with 3191 additions and 83 deletions

View File

@@ -818,8 +818,23 @@ ipcMain.handle(
ipcMain.handle("claude:check-cli", async () => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const info = claudeCliDetector.getInstallationInfo();
return { success: true, ...info };
const path = require("path");
const credentialsPath = path.join(app.getPath("userData"), "credentials.json");
const fullStatus = claudeCliDetector.getFullStatus(credentialsPath);
// Return in format expected by settings view (status: "installed" | "not_installed")
return {
success: true,
status: fullStatus.installed ? "installed" : "not_installed",
method: fullStatus.auth?.method || null,
version: fullStatus.version || null,
path: fullStatus.path || null,
authenticated: fullStatus.auth?.authenticated || false,
recommendation: fullStatus.installed
? null
: "Install Claude Code CLI for optimal performance with ultrathink.",
installCommands: fullStatus.installed ? null : claudeCliDetector.getInstallCommands(),
};
} catch (error) {
console.error("[IPC] claude:check-cli error:", error);
return { success: false, error: error.message };
@@ -1363,3 +1378,233 @@ ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => {
return { success: false, error: error.message };
}
});
// ============================================================================
// Setup & CLI Management IPC Handlers
// ============================================================================
/**
* Get comprehensive Claude CLI status including auth
*/
ipcMain.handle("setup:claude-status", async () => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const credentialsPath = path.join(app.getPath("userData"), "credentials.json");
const result = claudeCliDetector.getFullStatus(credentialsPath);
console.log("[IPC] setup:claude-status result:", result);
return result;
} catch (error) {
console.error("[IPC] setup:claude-status error:", error);
return { success: false, error: error.message };
}
});
/**
* Get comprehensive Codex CLI status including auth
*/
ipcMain.handle("setup:codex-status", async () => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const info = codexCliDetector.getFullStatus();
return { success: true, ...info };
} catch (error) {
console.error("[IPC] setup:codex-status error:", error);
return { success: false, error: error.message };
}
});
/**
* Install Claude CLI
*/
ipcMain.handle("setup:install-claude", async (event) => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:install-progress", {
cli: "claude",
...progress
});
}
};
const result = await claudeCliDetector.installCli(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:install-claude error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Install Codex CLI
*/
ipcMain.handle("setup:install-codex", async (event) => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:install-progress", {
cli: "codex",
...progress
});
}
};
const result = await codexCliDetector.installCli(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:install-codex error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Authenticate Claude CLI (manual auth required)
*/
ipcMain.handle("setup:auth-claude", async (event) => {
try {
const claudeCliDetector = require("./services/claude-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:auth-progress", {
cli: "claude",
...progress
});
}
};
const result = await claudeCliDetector.runSetupToken(sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:auth-claude error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Authenticate Codex CLI with optional API key
*/
ipcMain.handle("setup:auth-codex", async (event, { apiKey }) => {
try {
const codexCliDetector = require("./services/codex-cli-detector");
const sendProgress = (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("setup:auth-progress", {
cli: "codex",
...progress
});
}
};
const result = await codexCliDetector.authenticate(apiKey, sendProgress);
return { success: true, ...result };
} catch (error) {
console.error("[IPC] setup:auth-codex error:", error);
return { success: false, error: error.message || error.error };
}
});
/**
* Store API key or OAuth token securely (using app's userData)
* @param {string} provider - Provider name (anthropic, openai, google, anthropic_oauth_token)
* @param {string} apiKey - The API key or OAuth token to store
*/
ipcMain.handle("setup:store-api-key", async (_, { provider, apiKey }) => {
try {
console.log("[IPC] setup:store-api-key called for provider:", provider);
const configPath = path.join(app.getPath("userData"), "credentials.json");
let credentials = {};
// Read existing credentials
try {
const content = await fs.readFile(configPath, "utf-8");
credentials = JSON.parse(content);
} catch (e) {
// File doesn't exist, start fresh
}
// Store the new key/token
credentials[provider] = apiKey;
// Write back
await fs.writeFile(configPath, JSON.stringify(credentials, null, 2), "utf-8");
console.log("[IPC] setup:store-api-key stored successfully for:", provider);
return { success: true };
} catch (error) {
console.error("[IPC] setup:store-api-key error:", error);
return { success: false, error: error.message };
}
});
/**
* Get stored API keys and tokens
*/
ipcMain.handle("setup:get-api-keys", async () => {
try {
const configPath = path.join(app.getPath("userData"), "credentials.json");
try {
const content = await fs.readFile(configPath, "utf-8");
const credentials = JSON.parse(content);
// Return which keys/tokens exist (not the actual values for security)
return {
success: true,
hasAnthropicKey: !!credentials.anthropic,
hasAnthropicOAuthToken: !!credentials.anthropic_oauth_token,
hasOpenAIKey: !!credentials.openai,
hasGoogleKey: !!credentials.google
};
} catch (e) {
return {
success: true,
hasAnthropicKey: false,
hasAnthropicOAuthToken: false,
hasOpenAIKey: false,
hasGoogleKey: false
};
}
} catch (error) {
console.error("[IPC] setup:get-api-keys error:", error);
return { success: false, error: error.message };
}
});
/**
* Configure Codex MCP server for a project
*/
ipcMain.handle("setup:configure-codex-mcp", async (_, { projectPath }) => {
try {
const codexConfigManager = require("./services/codex-config-manager");
const mcpServerPath = path.join(__dirname, "services", "mcp-server-factory.js");
const configPath = await codexConfigManager.configureMcpServer(projectPath, mcpServerPath);
return { success: true, configPath };
} catch (error) {
console.error("[IPC] setup:configure-codex-mcp error:", error);
return { success: false, error: error.message };
}
});
/**
* Get platform information
*/
ipcMain.handle("setup:get-platform", async () => {
const os = require("os");
return {
success: true,
platform: process.platform,
arch: process.arch,
homeDir: os.homedir(),
isWindows: process.platform === "win32",
isMac: process.platform === "darwin",
isLinux: process.platform === "linux"
};
});

View File

@@ -252,6 +252,59 @@ contextBridge.exposeInMainWorld("electronAPI", {
};
},
},
// Setup & CLI Management API
setup: {
// Get comprehensive Claude CLI status
getClaudeStatus: () => ipcRenderer.invoke("setup:claude-status"),
// Get comprehensive Codex CLI status
getCodexStatus: () => ipcRenderer.invoke("setup:codex-status"),
// Install Claude CLI
installClaude: () => ipcRenderer.invoke("setup:install-claude"),
// Install Codex CLI
installCodex: () => ipcRenderer.invoke("setup:install-codex"),
// Authenticate Claude CLI
authClaude: () => ipcRenderer.invoke("setup:auth-claude"),
// Authenticate Codex CLI with optional API key
authCodex: (apiKey) => ipcRenderer.invoke("setup:auth-codex", { apiKey }),
// Store API key securely
storeApiKey: (provider, apiKey) =>
ipcRenderer.invoke("setup:store-api-key", { provider, apiKey }),
// Get stored API keys status
getApiKeys: () => ipcRenderer.invoke("setup:get-api-keys"),
// Configure Codex MCP server for a project
configureCodexMcp: (projectPath) =>
ipcRenderer.invoke("setup:configure-codex-mcp", { projectPath }),
// Get platform information
getPlatform: () => ipcRenderer.invoke("setup:get-platform"),
// Listen for installation progress
onInstallProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:install-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:install-progress", subscription);
};
},
// Listen for auth progress
onAuthProgress: (callback) => {
const subscription = (_, data) => callback(data);
ipcRenderer.on("setup:auth-progress", subscription);
return () => {
ipcRenderer.removeListener("setup:auth-progress", subscription);
};
},
},
});
// Also expose a flag to detect if we're in Electron

View File

@@ -1,71 +1,185 @@
const { execSync } = require('child_process');
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
/**
* Claude CLI Detector
*
* Authentication options:
* 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token to the app
* 2. API Key (Pay-per-use): User provides their Anthropic API key directly
*/
class ClaudeCliDetector {
/**
* Check if Claude Code CLI is installed and accessible
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'sdk'|'none' }
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'none' }
*/
static detectClaudeInstallation() {
try {
// Method 1: Check if 'claude' command is in PATH
try {
const claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
} catch (error) {
// CLI not in PATH, check local installation
}
// Method 2: Check for local installation
const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude');
if (fs.existsSync(localClaudePath)) {
/**
* Try to get updated PATH from shell config files
* This helps detect CLI installations that modify shell config but haven't updated the current process PATH
*/
static getUpdatedPathFromShellConfig() {
const homeDir = os.homedir();
const shell = process.env.SHELL || '/bin/bash';
const shellName = path.basename(shell);
// Common shell config files
const configFiles = [];
if (shellName.includes('zsh')) {
configFiles.push(path.join(homeDir, '.zshrc'));
configFiles.push(path.join(homeDir, '.zshenv'));
configFiles.push(path.join(homeDir, '.zprofile'));
} else if (shellName.includes('bash')) {
configFiles.push(path.join(homeDir, '.bashrc'));
configFiles.push(path.join(homeDir, '.bash_profile'));
configFiles.push(path.join(homeDir, '.profile'));
}
// Also check common locations
const commonPaths = [
path.join(homeDir, '.local', 'bin'),
path.join(homeDir, '.cargo', 'bin'),
'/usr/local/bin',
'/opt/homebrew/bin',
path.join(homeDir, 'bin'),
];
// Try to extract PATH additions from config files
for (const configFile of configFiles) {
if (fs.existsSync(configFile)) {
try {
const version = execSync(`${localClaudePath} --version`, { encoding: 'utf-8' }).trim();
return {
installed: true,
path: localClaudePath,
version: version,
method: 'cli-local'
};
const content = fs.readFileSync(configFile, 'utf-8');
// Look for PATH exports that might include claude installation paths
const pathMatches = content.match(/export\s+PATH=["']?([^"'\n]+)["']?/g);
if (pathMatches) {
for (const match of pathMatches) {
const pathValue = match.replace(/export\s+PATH=["']?/, '').replace(/["']?$/, '');
const paths = pathValue.split(':').filter(p => p && !p.includes('$'));
commonPaths.push(...paths);
}
}
} catch (error) {
// Local CLI exists but may not be executable
// Ignore errors reading config files
}
}
}
return [...new Set(commonPaths)]; // Remove duplicates
}
static detectClaudeInstallation() {
console.log('[ClaudeCliDetector] Detecting Claude installation...');
try {
// Method 1: Check if 'claude' command is in PATH (Unix)
if (process.platform !== 'win32') {
try {
const claudePath = execSync('which claude 2>/dev/null', { encoding: 'utf-8' }).trim();
if (claudePath) {
const version = this.getClaudeVersion(claudePath);
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
}
} catch (error) {
// CLI not in PATH, continue checking other locations
}
}
// Method 3: Check Windows path
// Method 2: Check Windows path
if (process.platform === 'win32') {
try {
const claudePath = execSync('where claude', { encoding: 'utf-8' }).trim();
const version = execSync('claude --version', { encoding: 'utf-8' }).trim();
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
const claudePath = execSync('where claude 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0];
if (claudePath) {
const version = this.getClaudeVersion(claudePath);
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
}
} catch (error) {
// Not found
// Not found on Windows
}
}
// Method 4: SDK mode (using OAuth token)
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
// Method 3: Check for local installation
const localClaudePath = path.join(os.homedir(), '.claude', 'local', 'claude');
if (fs.existsSync(localClaudePath)) {
const version = this.getClaudeVersion(localClaudePath);
console.log('[ClaudeCliDetector] Found local claude at:', localClaudePath, 'version:', version);
return {
installed: true,
path: null,
version: 'SDK Mode',
method: 'sdk'
path: localClaudePath,
version: version,
method: 'cli-local'
};
}
// Method 4: Check common installation locations (including those from shell config)
const commonPaths = this.getUpdatedPathFromShellConfig();
const binaryNames = ['claude', 'claude-code'];
for (const basePath of commonPaths) {
for (const binaryName of binaryNames) {
const claudePath = path.join(basePath, binaryName);
if (fs.existsSync(claudePath)) {
try {
const version = this.getClaudeVersion(claudePath);
console.log('[ClaudeCliDetector] Found claude at:', claudePath, 'version:', version);
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
} catch (error) {
// File exists but can't get version, might not be executable
}
}
}
}
// Method 5: Try to source shell config and check PATH again (for Unix)
if (process.platform !== 'win32') {
try {
const shell = process.env.SHELL || '/bin/bash';
const shellName = path.basename(shell);
const homeDir = os.homedir();
let sourceCmd = '';
if (shellName.includes('zsh')) {
sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`;
} else if (shellName.includes('bash')) {
sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`;
}
if (sourceCmd) {
const claudePath = execSync(`bash -c "${sourceCmd}"`, { encoding: 'utf-8', timeout: 2000 }).trim();
if (claudePath && claudePath.startsWith('/')) {
const version = this.getClaudeVersion(claudePath);
console.log('[ClaudeCliDetector] Found claude via shell config at:', claudePath, 'version:', version);
return {
installed: true,
path: claudePath,
version: version,
method: 'cli'
};
}
}
} catch (error) {
// Failed to source shell config or find claude
}
}
console.log('[ClaudeCliDetector] Claude CLI not found');
return {
installed: false,
path: null,
@@ -85,35 +199,223 @@ class ClaudeCliDetector {
}
/**
* Get installation recommendations
* Get Claude CLI version
* @param {string} claudePath Path to claude executable
* @returns {string|null} Version string or null
*/
static getInstallationInfo() {
static getClaudeVersion(claudePath) {
try {
const version = execSync(`"${claudePath}" --version 2>/dev/null`, {
encoding: 'utf-8',
timeout: 5000
}).trim();
return version || null;
} catch (error) {
return null;
}
}
/**
* Get authentication status
* Checks for:
* 1. OAuth token stored in app's credentials (from `claude setup-token`)
* 2. API key stored in app's credentials
* 3. API key in environment variable
*
* @param {string} appCredentialsPath Path to app's credentials.json
* @returns {Object} Authentication status
*/
static getAuthStatus(appCredentialsPath) {
console.log('[ClaudeCliDetector] Checking auth status...');
const envApiKey = process.env.ANTHROPIC_API_KEY;
console.log('[ClaudeCliDetector] Env ANTHROPIC_API_KEY:', !!envApiKey);
// Check app's stored credentials
let storedOAuthToken = null;
let storedApiKey = null;
if (appCredentialsPath && fs.existsSync(appCredentialsPath)) {
try {
const content = fs.readFileSync(appCredentialsPath, 'utf-8');
const credentials = JSON.parse(content);
storedOAuthToken = credentials.anthropic_oauth_token || null;
storedApiKey = credentials.anthropic || credentials.anthropic_api_key || null;
console.log('[ClaudeCliDetector] App credentials:', {
hasOAuthToken: !!storedOAuthToken,
hasApiKey: !!storedApiKey
});
} catch (error) {
console.error('[ClaudeCliDetector] Error reading app credentials:', error);
}
}
// Determine authentication method
// Priority: Stored OAuth Token > Stored API Key > Env API Key
let authenticated = false;
let method = 'none';
if (storedOAuthToken) {
authenticated = true;
method = 'oauth_token';
console.log('[ClaudeCliDetector] Using stored OAuth token (subscription)');
} else if (storedApiKey) {
authenticated = true;
method = 'api_key';
console.log('[ClaudeCliDetector] Using stored API key');
} else if (envApiKey) {
authenticated = true;
method = 'api_key_env';
console.log('[ClaudeCliDetector] Using environment API key');
} else {
console.log('[ClaudeCliDetector] No authentication found');
}
const result = {
authenticated,
method,
hasStoredOAuthToken: !!storedOAuthToken,
hasStoredApiKey: !!storedApiKey,
hasEnvApiKey: !!envApiKey
};
console.log('[ClaudeCliDetector] Auth status result:', result);
return result;
}
/**
* Get full status including installation and auth
* @param {string} appCredentialsPath Path to app's credentials.json
* @returns {Object} Full status
*/
static getFullStatus(appCredentialsPath) {
const installation = this.detectClaudeInstallation();
const auth = this.getAuthStatus(appCredentialsPath);
return {
success: true,
status: installation.installed ? 'installed' : 'not_installed',
installed: installation.installed,
path: installation.path,
version: installation.version,
method: installation.method,
auth
};
}
/**
* Get installation commands for different platforms
* @returns {Object} Installation commands
*/
static getInstallCommands() {
return {
macos: 'curl -fsSL https://claude.ai/install.sh | bash',
windows: 'irm https://claude.ai/install.ps1 | iex',
linux: 'curl -fsSL https://claude.ai/install.sh | bash'
};
}
/**
* Install Claude CLI using the official script
* @param {Function} onProgress Callback for progress updates
* @returns {Promise<Object>} Installation result
*/
static async installCli(onProgress) {
return new Promise((resolve, reject) => {
const platform = process.platform;
let command, args;
if (platform === 'win32') {
command = 'powershell';
args = ['-Command', 'irm https://claude.ai/install.ps1 | iex'];
} else {
command = 'bash';
args = ['-c', 'curl -fsSL https://claude.ai/install.sh | bash'];
}
console.log('[ClaudeCliDetector] Installing Claude CLI...');
const proc = spawn(command, args, {
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) {
console.log('[ClaudeCliDetector] Installation completed successfully');
resolve({
success: true,
output,
message: 'Claude CLI installed successfully'
});
} else {
console.error('[ClaudeCliDetector] Installation failed with code:', code);
reject({
success: false,
error: errorOutput || `Installation failed with code ${code}`,
output
});
}
});
proc.on('error', (error) => {
console.error('[ClaudeCliDetector] Installation error:', error);
reject({
success: false,
error: error.message,
output
});
});
});
}
/**
* Get instructions for setup-token command
* @returns {Object} Setup token instructions
*/
static getSetupTokenInstructions() {
const detection = this.detectClaudeInstallation();
if (detection.installed) {
if (!detection.installed) {
return {
status: 'installed',
method: detection.method,
version: detection.version,
path: detection.path,
recommendation: detection.method === 'cli'
? 'Using Claude Code CLI - optimal for long-running tasks'
: 'Using SDK mode - works well but CLI may provide better performance'
success: false,
error: 'Claude CLI is not installed. Please install it first.',
installCommands: this.getInstallCommands()
};
}
return {
status: 'not_installed',
recommendation: 'Consider installing Claude Code CLI for better performance with ultrathink',
installCommands: {
macos: 'curl -fsSL claude.ai/install.sh | bash',
windows: 'irm https://claude.ai/install.ps1 | iex',
linux: 'curl -fsSL claude.ai/install.sh | bash',
npm: 'npm install -g @anthropic-ai/claude-code'
}
success: true,
command: 'claude setup-token',
instructions: [
'1. Open your terminal',
'2. Run: claude setup-token',
'3. Follow the prompts to authenticate',
'4. Copy the token that is displayed',
'5. Paste the token in the field below'
],
note: 'This token is from your Claude subscription and allows you to use Claude without API charges.'
};
}
}
module.exports = ClaudeCliDetector;

View File

@@ -1,4 +1,4 @@
const { execSync } = require('child_process');
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
@@ -11,6 +11,205 @@ const os = require('os');
* 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)) {
const content = fs.readFileSync(authPath, 'utf-8');
const auth = JSON.parse(content);
// 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) {
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
}
// Check for various possible auth fields that codex might use
if (auth.api_key || auth.openai_api_key || auth.access_token || auth.apiKey) {
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
// Also check if the file has any meaningful content (non-empty object)
const keys = Object.keys(auth);
if (keys.length > 0) {
// 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
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
} catch (cmdError) {
// Command failed, but file exists - might still be authenticated
// Return authenticated if file has content
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
}
} catch (verifyError) {
// Verification failed, but file exists with content
return {
authenticated: true,
method: 'auth_file',
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath
};
}
}
}
// 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' }
@@ -224,6 +423,171 @@ class CodexCliDetector {
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;

View File

@@ -408,16 +408,16 @@ class FeatureExecutor {
if (provider?.ensureAuthEnv && !provider.ensureAuthEnv()) {
// Check if CLI is installed to provide better error message
let authMsg =
"Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.";
"Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.";
try {
const claudeCliDetector = require("./claude-cli-detector");
const detection = claudeCliDetector.detectClaudeInstallation();
if (detection.installed && detection.method === "cli") {
authMsg =
"Claude CLI is installed but not authenticated. Run `claude login` to authenticate, or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.";
"Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.";
} else {
authMsg =
"Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, or install Claude CLI and run `claude login`.";
"Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.";
}
} catch (err) {
// Fallback to default message

View File

@@ -94,9 +94,42 @@ class ClaudeProvider extends ModelProvider {
this.sdk = null;
}
/**
* Try to load credentials from the app's own credentials.json file.
* This is where we store OAuth tokens and API keys that users enter in the setup wizard.
* Returns { oauthToken, apiKey } or null values if not found.
*/
loadTokenFromAppCredentials() {
try {
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
const credentialsPath = path.join(app.getPath('userData'), 'credentials.json');
if (!fs.existsSync(credentialsPath)) {
console.log('[ClaudeProvider] App credentials file does not exist:', credentialsPath);
return { oauthToken: null, apiKey: null };
}
const raw = fs.readFileSync(credentialsPath, 'utf-8');
const parsed = JSON.parse(raw);
// Check for OAuth token first (from claude setup-token), then API key
const oauthToken = parsed.anthropic_oauth_token || null;
const apiKey = parsed.anthropic || parsed.anthropic_api_key || null;
console.log('[ClaudeProvider] App credentials check - OAuth token:', !!oauthToken, ', API key:', !!apiKey);
return { oauthToken, apiKey };
} catch (err) {
console.warn('[ClaudeProvider] Failed to read app credentials:', err?.message);
return { oauthToken: null, apiKey: null };
}
}
/**
* Try to load a Claude OAuth token from the local CLI config (~/.claude/config.json).
* Returns the token string or null if not found.
* NOTE: Claude's credentials.json is encrypted, so we only try config.json
*/
loadTokenFromCliConfig() {
try {
@@ -117,30 +150,44 @@ class ClaudeProvider extends ModelProvider {
}
ensureAuthEnv() {
// If API key or token already present, keep as-is.
// If API key or token already present in environment, keep as-is.
if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) {
console.log('[ClaudeProvider] Auth already present in environment');
return true;
}
// Try to hydrate from CLI login config
// Priority 1: Try to load from app's own credentials (setup wizard)
const appCredentials = this.loadTokenFromAppCredentials();
if (appCredentials.oauthToken) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = appCredentials.oauthToken;
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from app credentials');
return true;
}
if (appCredentials.apiKey) {
process.env.ANTHROPIC_API_KEY = appCredentials.apiKey;
console.log('[ClaudeProvider] Loaded ANTHROPIC_API_KEY from app credentials');
return true;
}
// Priority 2: Try to hydrate from CLI login config (legacy)
const token = this.loadTokenFromCliConfig();
if (token) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from ~/.claude/config.json');
return true;
}
// Check if CLI is installed but not logged in
try {
const claudeCliDetector = require('./claude-cli-detector');
const detection = claudeCliDetector.detectClaudeInstallation();
if (detection.installed && detection.method === 'cli') {
console.error('[ClaudeProvider] Claude CLI is installed but not logged in. Run `claude login` to authenticate.');
console.error('[ClaudeProvider] Claude CLI is installed but not authenticated. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.');
} else {
console.error('[ClaudeProvider] No Anthropic auth found (env empty, ~/.claude/config.json missing token)');
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
}
} catch (err) {
console.error('[ClaudeProvider] No Anthropic auth found (env empty, ~/.claude/config.json missing token)');
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
}
return false;
}
@@ -156,17 +203,17 @@ class ClaudeProvider extends ModelProvider {
}
async *executeQuery(options) {
// Ensure we have auth; fall back to CLI login token if available.
// Ensure we have auth; fall back to app credentials or CLI login token if available.
if (!this.ensureAuthEnv()) {
// Check if CLI is installed to provide better error message
let msg = 'Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.';
let msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.';
try {
const claudeCliDetector = require('./claude-cli-detector');
const detection = claudeCliDetector.detectClaudeInstallation();
if (detection.installed && detection.method === 'cli') {
msg = 'Claude CLI is installed but not authenticated. Run `claude login` to authenticate, or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.';
msg = 'Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.';
} else {
msg = 'Missing Anthropic auth. Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN, or install Claude CLI and run `claude login`.';
msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.';
}
} catch (err) {
// Fallback to default message
@@ -239,11 +286,11 @@ class ClaudeProvider extends ModelProvider {
validateConfig() {
const errors = [];
// Ensure auth is available (try to auto-load from CLI config)
// Ensure auth is available (try to auto-load from app credentials or CLI config)
this.ensureAuthEnv();
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_API_KEY) {
errors.push('No Claude authentication found. Set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY, or run `claude login` to populate ~/.claude/config.json.');
errors.push('No Claude authentication found. Go to Settings > Setup to configure your subscription token or API key.');
}
return {

View File

@@ -11,11 +11,14 @@ import { AgentToolsView } from "@/components/views/agent-tools-view";
import { InterviewView } from "@/components/views/interview-view";
import { ContextView } from "@/components/views/context-view";
import { ProfilesView } from "@/components/views/profiles-view";
import { SetupView } from "@/components/views/setup-view";
import { useAppStore } from "@/store/app-store";
import { useSetupStore } from "@/store/setup-store";
import { getElectronAPI, isElectron } from "@/lib/electron";
export default function Home() {
const { currentView, setIpcConnected, theme } = useAppStore();
const { currentView, setCurrentView, setIpcConnected, theme } = useAppStore();
const { isFirstRun, setupComplete } = useSetupStore();
const [isMounted, setIsMounted] = useState(false);
// Prevent hydration issues
@@ -23,6 +26,24 @@ export default function Home() {
setIsMounted(true);
}, []);
// Check if this is first run and redirect to setup if needed
useEffect(() => {
console.log("[Setup Flow] Checking setup state:", {
isMounted,
isFirstRun,
setupComplete,
currentView,
shouldShowSetup: isMounted && isFirstRun && !setupComplete,
});
if (isMounted && isFirstRun && !setupComplete) {
console.log("[Setup Flow] Redirecting to setup wizard (first run, not complete)");
setCurrentView("setup");
} else if (isMounted && setupComplete) {
console.log("[Setup Flow] Setup already complete, showing normal view");
}
}, [isMounted, isFirstRun, setupComplete, setCurrentView, currentView]);
// Test IPC connection on mount
useEffect(() => {
const testConnection = async () => {
@@ -96,6 +117,8 @@ export default function Home() {
switch (currentView) {
case "welcome":
return <WelcomeView />;
case "setup":
return <SetupView />;
case "board":
return <BoardView />;
case "spec":
@@ -117,6 +140,21 @@ export default function Home() {
}
};
// Setup view is full-screen without sidebar
if (currentView === "setup") {
return (
<main className="h-screen overflow-hidden" data-testid="app-container">
<SetupView />
{/* Environment indicator */}
{isMounted && !isElectron() && (
<div className="fixed bottom-4 right-4 px-3 py-1.5 bg-yellow-500/10 text-yellow-500 text-xs rounded-full border border-yellow-500/20 pointer-events-none">
Web Mode (Mock IPC)
</div>
)}
</main>
);
}
return (
<main className="flex h-screen overflow-hidden" data-testid="app-container">
<Sidebar />

File diff suppressed because it is too large Load Diff

View File

@@ -172,6 +172,54 @@ export interface ElectronAPI {
git?: GitAPI;
suggestions?: SuggestionsAPI;
specRegeneration?: SpecRegenerationAPI;
setup?: {
getClaudeStatus: () => Promise<{
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasCredentialsFile: boolean;
hasToken: boolean;
};
error?: string;
}>;
getCodexStatus: () => Promise<{
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasAuthFile: boolean;
hasEnvKey: boolean;
};
error?: string;
}>;
installClaude: () => Promise<{ success: boolean; message?: string; error?: string }>;
installCodex: () => Promise<{ success: boolean; message?: string; error?: string }>;
authClaude: () => Promise<{ success: boolean; requiresManualAuth?: boolean; command?: string; error?: string }>;
authCodex: (apiKey?: string) => Promise<{ success: boolean; requiresManualAuth?: boolean; command?: string; error?: string }>;
storeApiKey: (provider: string, apiKey: string) => Promise<{ success: boolean; error?: string }>;
getApiKeys: () => Promise<{ success: boolean; hasAnthropicKey: boolean; hasOpenAIKey: boolean; hasGoogleKey: boolean }>;
configureCodexMcp: (projectPath: string) => Promise<{ success: boolean; configPath?: string; error?: string }>;
getPlatform: () => Promise<{
success: boolean;
platform: string;
arch: string;
homeDir: string;
isWindows: boolean;
isMac: boolean;
isLinux: boolean;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void;
};
}
// Note: Window interface is declared in @/types/electron.d.ts
@@ -461,6 +509,9 @@ export const getElectronAPI = (): ElectronAPI => {
error: "OpenAI connection test is only available in the Electron app.",
}),
// Mock Setup API
setup: createMockSetupAPI(),
// Mock Auto Mode API
autoMode: createMockAutoModeAPI(),
@@ -478,6 +529,175 @@ export const getElectronAPI = (): ElectronAPI => {
};
};
// Setup API interface
interface SetupAPI {
getClaudeStatus: () => Promise<{
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasCredentialsFile: boolean;
hasToken: boolean;
};
error?: string;
}>;
getCodexStatus: () => Promise<{
success: boolean;
status?: string;
method?: string;
version?: string;
path?: string;
auth?: {
authenticated: boolean;
method: string;
hasAuthFile: boolean;
hasEnvKey: boolean;
};
error?: string;
}>;
installClaude: () => Promise<{ success: boolean; message?: string; error?: string }>;
installCodex: () => Promise<{ success: boolean; message?: string; error?: string }>;
authClaude: () => Promise<{ success: boolean; requiresManualAuth?: boolean; command?: string; error?: string }>;
authCodex: (apiKey?: string) => Promise<{ success: boolean; requiresManualAuth?: boolean; command?: string; error?: string }>;
storeApiKey: (provider: string, apiKey: string) => Promise<{ success: boolean; error?: string }>;
getApiKeys: () => Promise<{ success: boolean; hasAnthropicKey: boolean; hasOpenAIKey: boolean; hasGoogleKey: boolean }>;
configureCodexMcp: (projectPath: string) => Promise<{ success: boolean; configPath?: string; error?: string }>;
getPlatform: () => Promise<{
success: boolean;
platform: string;
arch: string;
homeDir: string;
isWindows: boolean;
isMac: boolean;
isLinux: boolean;
}>;
onInstallProgress?: (callback: (progress: any) => void) => () => void;
onAuthProgress?: (callback: (progress: any) => void) => () => void;
}
// Mock Setup API implementation
function createMockSetupAPI(): SetupAPI {
return {
getClaudeStatus: async () => {
console.log("[Mock] Getting Claude status");
return {
success: true,
status: "not_installed",
auth: {
authenticated: false,
method: "none",
hasCredentialsFile: false,
hasToken: false,
},
};
},
getCodexStatus: async () => {
console.log("[Mock] Getting Codex status");
return {
success: true,
status: "not_installed",
auth: {
authenticated: false,
method: "none",
hasAuthFile: false,
hasEnvKey: false,
},
};
},
installClaude: async () => {
console.log("[Mock] Installing Claude CLI");
// Simulate installation delay
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
success: false,
error: "CLI installation is only available in the Electron app. Please run the command manually.",
};
},
installCodex: async () => {
console.log("[Mock] Installing Codex CLI");
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
success: false,
error: "CLI installation is only available in the Electron app. Please run the command manually.",
};
},
authClaude: async () => {
console.log("[Mock] Auth Claude CLI");
return {
success: true,
requiresManualAuth: true,
command: "claude login",
};
},
authCodex: async (apiKey?: string) => {
console.log("[Mock] Auth Codex CLI", { hasApiKey: !!apiKey });
if (apiKey) {
return { success: true };
}
return {
success: true,
requiresManualAuth: true,
command: "codex auth login",
};
},
storeApiKey: async (provider: string, apiKey: string) => {
console.log("[Mock] Storing API key for:", provider);
// In mock mode, we just pretend to store it (it's already in the app store)
return { success: true };
},
getApiKeys: async () => {
console.log("[Mock] Getting API keys");
return {
success: true,
hasAnthropicKey: false,
hasOpenAIKey: false,
hasGoogleKey: false,
};
},
configureCodexMcp: async (projectPath: string) => {
console.log("[Mock] Configuring Codex MCP for:", projectPath);
return {
success: true,
configPath: `${projectPath}/.codex/config.toml`,
};
},
getPlatform: async () => {
return {
success: true,
platform: "darwin",
arch: "arm64",
homeDir: "/Users/mock",
isWindows: false,
isMac: true,
isLinux: false,
};
},
onInstallProgress: (callback) => {
// Mock progress events
return () => {};
},
onAuthProgress: (callback) => {
// Mock auth events
return () => {};
},
};
}
// Mock Worktree API implementation
function createMockWorktreeAPI(): WorktreeAPI {
return {

View File

@@ -4,6 +4,7 @@ import type { Project, TrashedProject } from "@/lib/electron";
export type ViewMode =
| "welcome"
| "setup"
| "spec"
| "board"
| "agent"

View File

@@ -0,0 +1,182 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
// CLI Installation Status
export interface CliStatus {
installed: boolean;
path: string | null;
version: string | null;
method: string;
error?: string;
}
// Claude Auth Status
export interface ClaudeAuthStatus {
authenticated: boolean;
method: "oauth" | "api_key" | "none";
hasCredentialsFile: boolean;
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
error?: string;
}
// Codex Auth Status
export interface CodexAuthStatus {
authenticated: boolean;
method: "api_key" | "env" | "none";
apiKeyValid?: boolean;
mcpConfigured?: boolean;
error?: string;
}
// Installation Progress
export interface InstallProgress {
isInstalling: boolean;
currentStep: string;
progress: number; // 0-100
output: string[];
error?: string;
}
export type SetupStep =
| "welcome"
| "claude_detect"
| "claude_auth"
| "codex_detect"
| "codex_auth"
| "complete";
export interface SetupState {
// Setup wizard state
isFirstRun: boolean;
setupComplete: boolean;
currentStep: SetupStep;
// Claude CLI state
claudeCliStatus: CliStatus | null;
claudeAuthStatus: ClaudeAuthStatus | null;
claudeInstallProgress: InstallProgress;
// Codex CLI state
codexCliStatus: CliStatus | null;
codexAuthStatus: CodexAuthStatus | null;
codexInstallProgress: InstallProgress;
// Setup preferences
skipClaudeSetup: boolean;
skipCodexSetup: boolean;
}
export interface SetupActions {
// Setup flow
setCurrentStep: (step: SetupStep) => void;
completeSetup: () => void;
resetSetup: () => void;
setIsFirstRun: (isFirstRun: boolean) => void;
// Claude CLI
setClaudeCliStatus: (status: CliStatus | null) => void;
setClaudeAuthStatus: (status: ClaudeAuthStatus | null) => void;
setClaudeInstallProgress: (progress: Partial<InstallProgress>) => void;
resetClaudeInstallProgress: () => void;
// Codex CLI
setCodexCliStatus: (status: CliStatus | null) => void;
setCodexAuthStatus: (status: CodexAuthStatus | null) => void;
setCodexInstallProgress: (progress: Partial<InstallProgress>) => void;
resetCodexInstallProgress: () => void;
// Preferences
setSkipClaudeSetup: (skip: boolean) => void;
setSkipCodexSetup: (skip: boolean) => void;
}
const initialInstallProgress: InstallProgress = {
isInstalling: false,
currentStep: "",
progress: 0,
output: [],
};
const initialState: SetupState = {
isFirstRun: true,
setupComplete: false,
currentStep: "welcome",
claudeCliStatus: null,
claudeAuthStatus: null,
claudeInstallProgress: { ...initialInstallProgress },
codexCliStatus: null,
codexAuthStatus: null,
codexInstallProgress: { ...initialInstallProgress },
skipClaudeSetup: false,
skipCodexSetup: false,
};
export const useSetupStore = create<SetupState & SetupActions>()(
persist(
(set, get) => ({
...initialState,
// Setup flow
setCurrentStep: (step) => set({ currentStep: step }),
completeSetup: () => set({ setupComplete: true, currentStep: "complete" }),
resetSetup: () => set({
...initialState,
isFirstRun: false, // Don't reset first run flag
}),
setIsFirstRun: (isFirstRun) => set({ isFirstRun }),
// Claude CLI
setClaudeCliStatus: (status) => set({ claudeCliStatus: status }),
setClaudeAuthStatus: (status) => set({ claudeAuthStatus: status }),
setClaudeInstallProgress: (progress) => set({
claudeInstallProgress: {
...get().claudeInstallProgress,
...progress,
},
}),
resetClaudeInstallProgress: () => set({
claudeInstallProgress: { ...initialInstallProgress },
}),
// Codex CLI
setCodexCliStatus: (status) => set({ codexCliStatus: status }),
setCodexAuthStatus: (status) => set({ codexAuthStatus: status }),
setCodexInstallProgress: (progress) => set({
codexInstallProgress: {
...get().codexInstallProgress,
...progress,
},
}),
resetCodexInstallProgress: () => set({
codexInstallProgress: { ...initialInstallProgress },
}),
// Preferences
setSkipClaudeSetup: (skip) => set({ skipClaudeSetup: skip }),
setSkipCodexSetup: (skip) => set({ skipCodexSetup: skip }),
}),
{
name: "automaker-setup",
partialize: (state) => ({
isFirstRun: state.isFirstRun,
setupComplete: state.setupComplete,
skipClaudeSetup: state.skipClaudeSetup,
skipCodexSetup: state.skipCodexSetup,
}),
}
)
);

View File

@@ -2338,3 +2338,173 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
options
);
}
// ============================================================================
// Setup View Utilities
// ============================================================================
/**
* Set up the app store to show setup view (simulate first run)
*/
export async function setupFirstRun(page: Page): Promise<void> {
await page.addInitScript(() => {
// Clear any existing setup state to simulate first run
localStorage.removeItem("automaker-setup");
localStorage.removeItem("automaker-storage");
// Set up the setup store state for first run
const setupState = {
state: {
isFirstRun: true,
setupComplete: false,
currentStep: "welcome",
claudeCliStatus: null,
claudeAuthStatus: null,
claudeInstallProgress: {
isInstalling: false,
currentStep: "",
progress: 0,
output: [],
},
codexCliStatus: null,
codexAuthStatus: null,
codexInstallProgress: {
isInstalling: false,
currentStep: "",
progress: 0,
output: [],
},
skipClaudeSetup: false,
skipCodexSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
// Also set up app store to show setup view
const appState = {
state: {
projects: [],
currentProject: null,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "", openai: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
isAutoModeRunning: false,
runningAutoTasks: [],
autoModeActivityLog: [],
currentView: "setup",
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(appState));
});
}
/**
* Set up the app to skip the setup wizard (setup already complete)
*/
export async function setupComplete(page: Page): Promise<void> {
await page.addInitScript(() => {
// Mark setup as complete
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
skipClaudeSetup: false,
skipCodexSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
});
}
/**
* Navigate to the setup view directly
*/
export async function navigateToSetup(page: Page): Promise<void> {
await setupFirstRun(page);
await page.goto("/");
await page.waitForLoadState("networkidle");
await waitForElement(page, "setup-view", { timeout: 10000 });
}
/**
* Wait for setup view to be visible
*/
export async function waitForSetupView(page: Page): Promise<Locator> {
return waitForElement(page, "setup-view", { timeout: 10000 });
}
/**
* Click "Get Started" button on setup welcome step
*/
export async function clickSetupGetStarted(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-start-button");
await button.click();
}
/**
* Click continue on Claude setup step
*/
export async function clickClaudeContinue(page: Page): Promise<void> {
const button = await getByTestId(page, "claude-next-button");
await button.click();
}
/**
* Click continue on Codex setup step
*/
export async function clickCodexContinue(page: Page): Promise<void> {
const button = await getByTestId(page, "codex-next-button");
await button.click();
}
/**
* Click finish on setup complete step
*/
export async function clickSetupFinish(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-finish-button");
await button.click();
}
/**
* Enter Anthropic API key in setup
*/
export async function enterAnthropicApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Use Anthropic API Key Instead" button
const useApiKeyButton = await getByTestId(page, "use-api-key-button");
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "anthropic-api-key-input");
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-anthropic-key-button");
await saveButton.click();
}
/**
* Enter OpenAI API key in setup
*/
export async function enterOpenAIApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Enter OpenAI API Key" button
const useApiKeyButton = await getByTestId(page, "use-openai-key-button");
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "openai-api-key-input");
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-openai-key-button");
await saveButton.click();
}