From 2afb5ced902666d252d549dabdd4388291e12da9 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 19:07:13 +0100 Subject: [PATCH 01/10] feat(cli-refresh): add refresh buttons for CLI detection in settings view --- app/src/components/views/settings-view.tsx | 85 +++++++++++++++++++--- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index f3fe7581..57f2c839 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -38,6 +38,7 @@ import { GitBranch, TestTube, Settings2, + RefreshCw, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { Checkbox } from "@/components/ui/checkbox"; @@ -134,6 +135,8 @@ export function SettingsView() { } | null>(null); const [activeSection, setActiveSection] = useState("api-keys"); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); + const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); const scrollContainerRef = useRef(null); useEffect(() => { @@ -339,6 +342,36 @@ export function SettingsView() { } }; + const handleRefreshClaudeCli = useCallback(async () => { + setIsCheckingClaudeCli(true); + try { + const api = getElectronAPI(); + if (api?.checkClaudeCli) { + const status = await api.checkClaudeCli(); + setClaudeCliStatus(status); + } + } catch (error) { + console.error("Failed to refresh Claude CLI status:", error); + } finally { + setIsCheckingClaudeCli(false); + } + }, []); + + const handleRefreshCodexCli = useCallback(async () => { + setIsCheckingCodexCli(true); + try { + const api = getElectronAPI(); + if (api?.checkCodexCli) { + const status = await api.checkCodexCli(); + setCodexCliStatus(status); + } + } catch (error) { + console.error("Failed to refresh Codex CLI status:", error); + } finally { + setIsCheckingCodexCli(false); + } + }, []); + const handleSave = () => { setApiKeys({ anthropic: anthropicKey, @@ -729,11 +762,27 @@ export function SettingsView() { className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" >
-
- -

- Claude Code CLI -

+
+
+ +

+ Claude Code CLI +

+
+

Claude Code CLI provides better performance for long-running @@ -853,11 +902,27 @@ export function SettingsView() { className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6" >

-
- -

- OpenAI Codex CLI -

+
+
+ +

+ OpenAI Codex CLI +

+
+

Codex CLI enables GPT-5.1 Codex models for autonomous coding From 3bd28d30843708ac029a6c95cc209c3ed9a8d326 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 19:15:29 +0100 Subject: [PATCH 02/10] 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. --- app/electron/main.js | 249 ++- app/electron/preload.js | 53 + app/electron/services/claude-cli-detector.js | 430 ++++- app/electron/services/codex-cli-detector.js | 366 ++++- app/electron/services/feature-executor.js | 6 +- app/electron/services/model-provider.js | 71 +- app/src/app/page.tsx | 40 +- app/src/components/views/setup-view.tsx | 1486 ++++++++++++++++++ app/src/lib/electron.ts | 220 +++ app/src/store/app-store.ts | 1 + app/src/store/setup-store.ts | 182 +++ app/tests/utils.ts | 170 ++ 12 files changed, 3191 insertions(+), 83 deletions(-) create mode 100644 app/src/components/views/setup-view.tsx create mode 100644 app/src/store/setup-store.ts diff --git a/app/electron/main.js b/app/electron/main.js index 7083789e..43118263 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -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" + }; +}); diff --git a/app/electron/preload.js b/app/electron/preload.js index 65d2b03a..dcb07e2c 100644 --- a/app/electron/preload.js +++ b/app/electron/preload.js @@ -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 diff --git a/app/electron/services/claude-cli-detector.js b/app/electron/services/claude-cli-detector.js index 31030f0d..f8f4739e 100644 --- a/app/electron/services/claude-cli-detector.js +++ b/app/electron/services/claude-cli-detector.js @@ -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} 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; - diff --git a/app/electron/services/codex-cli-detector.js b/app/electron/services/codex-cli-detector.js index 1a60e3b3..f45a8db2 100644 --- a/app/electron/services/codex-cli-detector.js +++ b/app/electron/services/codex-cli-detector.js @@ -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} 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} 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; diff --git a/app/electron/services/feature-executor.js b/app/electron/services/feature-executor.js index 8ab6e14e..10feda61 100644 --- a/app/electron/services/feature-executor.js +++ b/app/electron/services/feature-executor.js @@ -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 diff --git a/app/electron/services/model-provider.js b/app/electron/services/model-provider.js index d5a31850..b07588c9 100644 --- a/app/electron/services/model-provider.js +++ b/app/electron/services/model-provider.js @@ -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 { diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 27cc5e90..d1fcf2cf 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -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 ; + case "setup": + return ; case "board": return ; case "spec": @@ -117,6 +140,21 @@ export default function Home() { } }; + // Setup view is full-screen without sidebar + if (currentView === "setup") { + return ( +
+ + {/* Environment indicator */} + {isMounted && !isElectron() && ( +
+ Web Mode (Mock IPC) +
+ )} +
+ ); + } + return (
diff --git a/app/src/components/views/setup-view.tsx b/app/src/components/views/setup-view.tsx new file mode 100644 index 00000000..229d955c --- /dev/null +++ b/app/src/components/views/setup-view.tsx @@ -0,0 +1,1486 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useSetupStore } from "@/store/setup-store"; +import { useAppStore } from "@/store/app-store"; +import { getElectronAPI } from "@/lib/electron"; +import { + CheckCircle2, + XCircle, + Loader2, + Terminal, + Key, + Sparkles, + ArrowRight, + ArrowLeft, + ExternalLink, + Copy, + AlertCircle, + RefreshCw, + Download, + Shield, +} from "lucide-react"; +import { toast } from "sonner"; + +// Step indicator component +function StepIndicator({ + currentStep, + totalSteps, +}: { + currentStep: number; + totalSteps: number; +}) { + return ( +
+ {Array.from({ length: totalSteps }).map((_, index) => ( +
+ ))} +
+ ); +} + +// CLI Status Badge +function StatusBadge({ + status, + label, +}: { + status: "installed" | "not_installed" | "checking" | "authenticated" | "not_authenticated"; + label: string; +}) { + const getStatusConfig = () => { + switch (status) { + case "installed": + case "authenticated": + return { + icon: , + className: "bg-green-500/10 text-green-500 border-green-500/20", + }; + case "not_installed": + case "not_authenticated": + return { + icon: , + className: "bg-red-500/10 text-red-500 border-red-500/20", + }; + case "checking": + return { + icon: , + className: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20", + }; + } + }; + + const config = getStatusConfig(); + + return ( +
+ {config.icon} + {label} +
+ ); +} + +// Terminal Output Component +function TerminalOutput({ lines }: { lines: string[] }) { + return ( +
+ {lines.map((line, index) => ( +
+ $ {line} +
+ ))} + {lines.length === 0 && ( +
Waiting for output...
+ )} +
+ ); +} + +// Welcome Step +function WelcomeStep({ onNext }: { onNext: () => void }) { + return ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Automaker Logo +
+ +
+

+ Welcome to Automaker +

+

+ Let's set up your development environment. We'll check for required + CLI tools and help you configure them. +

+
+ +
+ + + + + Claude CLI + + + +

+ Anthropic's powerful AI assistant for code generation and analysis +

+
+
+ + + + + + Codex CLI + + + +

+ OpenAI's GPT-5.1 Codex for advanced code generation tasks +

+
+
+
+ + +
+ ); +} + +// Claude Setup Step - 2 Authentication Options: +// 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token +// 2. API Key (Pay-per-use): User provides their Anthropic API key directly +function ClaudeSetupStep({ + onNext, + onBack, + onSkip, +}: { + onNext: () => void; + onBack: () => void; + onSkip: () => void; +}) { + const { + claudeCliStatus, + claudeAuthStatus, + claudeInstallProgress, + setClaudeCliStatus, + setClaudeAuthStatus, + setClaudeInstallProgress, + } = useSetupStore(); + const { setApiKeys, apiKeys } = useAppStore(); + + const [isChecking, setIsChecking] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); + const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>(null); + const [oauthToken, setOAuthToken] = useState(""); + const [apiKey, setApiKey] = useState(""); + const [isSaving, setIsSaving] = useState(false); + + const checkStatus = useCallback(async () => { + console.log("[Claude Setup] Starting status check..."); + setIsChecking(true); + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + // Debug: Check what's available + console.log("[Claude Setup] isElectron:", typeof window !== "undefined" && (window as any).isElectron); + console.log("[Claude Setup] electronAPI exists:", typeof window !== "undefined" && !!(window as any).electronAPI); + console.log("[Claude Setup] electronAPI.setup exists:", typeof window !== "undefined" && !!(window as any).electronAPI?.setup); + console.log("[Claude Setup] Setup API available:", !!setupApi); + + if (setupApi?.getClaudeStatus) { + const result = await setupApi.getClaudeStatus(); + console.log("[Claude Setup] Raw status result:", result); + + if (result.success) { + const cliStatus = { + installed: result.installed || result.status === "installed", + path: result.path || null, + version: result.version || null, + method: result.method || "none", + }; + console.log("[Claude Setup] CLI Status:", cliStatus); + setClaudeCliStatus(cliStatus); + + if (result.auth) { + const authStatus = { + authenticated: result.auth.authenticated, + method: result.auth.method === "oauth_token" + ? "oauth" + : result.auth.method?.includes("api_key") + ? "api_key" + : "none", + hasCredentialsFile: false, + oauthTokenValid: result.auth.hasStoredOAuthToken, + apiKeyValid: result.auth.hasStoredApiKey || result.auth.hasEnvApiKey, + }; + console.log("[Claude Setup] Auth Status:", authStatus); + setClaudeAuthStatus(authStatus as any); + } + } + } + } catch (error) { + console.error("[Claude Setup] Failed to check Claude status:", error); + } finally { + setIsChecking(false); + } + }, [setClaudeCliStatus, setClaudeAuthStatus]); + + useEffect(() => { + checkStatus(); + }, [checkStatus]); + + const handleInstall = async () => { + setIsInstalling(true); + setClaudeInstallProgress({ + isInstalling: true, + currentStep: "Downloading Claude CLI...", + progress: 0, + output: [], + }); + + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + if (setupApi?.installClaude) { + const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => { + if (progress.cli === "claude") { + setClaudeInstallProgress({ + output: [...claudeInstallProgress.output, progress.data || progress.type || ""], + }); + } + }); + + const result = await setupApi.installClaude(); + unsubscribe?.(); + + if (result.success) { + // Installation script completed, but CLI might not be immediately detectable + // Wait a bit for installation to complete and PATH to update, then retry status check + let retries = 5; + let detected = false; + + // Initial delay to let the installation script finish setting up + await new Promise(resolve => setTimeout(resolve, 1500)); + + for (let i = 0; i < retries; i++) { + // Check status + await checkStatus(); + + // Small delay to let state update + await new Promise(resolve => setTimeout(resolve, 300)); + + // Check if CLI is now detected by re-reading from store + const currentStatus = useSetupStore.getState().claudeCliStatus; + if (currentStatus?.installed) { + detected = true; + toast.success("Claude CLI installed and detected successfully"); + break; + } + + // Wait before next retry (longer delays for later retries) + if (i < retries - 1) { + await new Promise(resolve => setTimeout(resolve, 2000 + (i * 500))); + } + } + + // Show appropriate message based on detection + if (!detected) { + // Installation completed but CLI not detected - this is common if PATH wasn't updated in current process + toast.success("Claude CLI installation completed", { + description: "The CLI was installed but may need a terminal restart to be detected. You can continue with authentication if you have a token.", + duration: 7000, + }); + } + } else { + toast.error("Installation failed", { description: result.error }); + } + } + } catch (error) { + console.error("Failed to install Claude:", error); + toast.error("Installation failed"); + } finally { + setIsInstalling(false); + setClaudeInstallProgress({ isInstalling: false }); + } + }; + + const handleSaveOAuthToken = async () => { + console.log("[Claude Setup] Saving OAuth token..."); + if (!oauthToken.trim()) { + toast.error("Please enter the token from claude setup-token"); + return; + } + + setIsSaving(true); + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + if (setupApi?.storeApiKey) { + const result = await setupApi.storeApiKey("anthropic_oauth_token", oauthToken); + console.log("[Claude Setup] Store OAuth token result:", result); + + if (result.success) { + setClaudeAuthStatus({ + authenticated: true, + method: "oauth", + hasCredentialsFile: false, + oauthTokenValid: true, + }); + toast.success("Claude subscription token saved"); + setAuthMethod(null); + await checkStatus(); + } else { + toast.error("Failed to save token", { description: result.error }); + } + } + } catch (error) { + console.error("[Claude Setup] Failed to save OAuth token:", error); + toast.error("Failed to save token"); + } finally { + setIsSaving(false); + } + }; + + const handleSaveApiKey = async () => { + console.log("[Claude Setup] Saving API key..."); + if (!apiKey.trim()) { + toast.error("Please enter an API key"); + return; + } + + setIsSaving(true); + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + if (setupApi?.storeApiKey) { + const result = await setupApi.storeApiKey("anthropic", apiKey); + console.log("[Claude Setup] Store API key result:", result); + + if (result.success) { + setApiKeys({ ...apiKeys, anthropic: apiKey }); + setClaudeAuthStatus({ + authenticated: true, + method: "api_key", + hasCredentialsFile: false, + apiKeyValid: true, + }); + toast.success("Anthropic API key saved"); + setAuthMethod(null); + await checkStatus(); + } else { + toast.error("Failed to save API key", { description: result.error }); + } + } else { + // Web mode fallback + setApiKeys({ ...apiKeys, anthropic: apiKey }); + setClaudeAuthStatus({ + authenticated: true, + method: "api_key", + hasCredentialsFile: false, + apiKeyValid: true, + }); + toast.success("Anthropic API key saved"); + setAuthMethod(null); + } + } catch (error) { + console.error("[Claude Setup] Failed to save API key:", error); + toast.error("Failed to save API key"); + } finally { + setIsSaving(false); + } + }; + + const copyCommand = (command: string) => { + navigator.clipboard.writeText(command); + toast.success("Command copied to clipboard"); + }; + + const isAuthenticated = claudeAuthStatus?.authenticated || apiKeys.anthropic; + + const getAuthMethodLabel = () => { + if (!isAuthenticated) return null; + if (claudeAuthStatus?.method === "oauth") return "Subscription Token"; + if (apiKeys.anthropic || claudeAuthStatus?.method === "api_key") return "API Key"; + return "Authenticated"; + }; + + return ( +
+
+
+ +
+

+ Claude Setup +

+

+ Configure Claude for code generation +

+
+ + {/* Status Card */} + + +
+ Status + +
+
+ +
+ CLI Installation + {isChecking ? ( + + ) : claudeCliStatus?.installed ? ( + + ) : ( + + )} +
+ + {claudeCliStatus?.version && ( +
+ Version + {claudeCliStatus.version} +
+ )} + +
+ Authentication + {isAuthenticated ? ( +
+ + {getAuthMethodLabel() && ( + ({getAuthMethodLabel()}) + )} +
+ ) : ( + + )} +
+
+
+ + {/* Installation Section */} + {!claudeCliStatus?.installed && ( + + + + + Install Claude CLI + + Required for subscription-based authentication + + +
+ +
+ + curl -fsSL https://claude.ai/install.sh | bash + + +
+
+ +
+ +
+ + irm https://claude.ai/install.ps1 | iex + + +
+
+ + {claudeInstallProgress.isInstalling && } + + +
+
+ )} + + {/* Authentication Section */} + {!isAuthenticated && ( + + + + + Authentication + + Choose your authentication method + + + {/* Option 1: Subscription Token */} + {authMethod === "token" ? ( +
+
+ +
+

Subscription Token

+

Use your Claude subscription (no API charges)

+ + {claudeCliStatus?.installed ? ( + <> +
+

1. Run this command in your terminal:

+
+ + claude setup-token + + +
+
+ +
+ + setOAuthToken(e.target.value)} + className="bg-input border-border text-foreground" + data-testid="oauth-token-input" + /> +
+ +
+ + +
+ + ) : ( +
+

+ + Install Claude CLI first to use subscription authentication +

+
+ )} +
+
+
+ ) : authMethod === "api_key" ? ( + /* Option 2: API Key */ +
+
+ +
+

API Key

+

Pay-per-use with your Anthropic API key

+ +
+ + setApiKey(e.target.value)} + className="bg-input border-border text-foreground" + data-testid="anthropic-api-key-input" + /> +

+ Get your API key from{" "} + + console.anthropic.com + + +

+
+ +
+ + +
+
+
+
+ ) : ( + /* Auth Method Selection */ +
+ + + +
+ )} +
+
+ )} + + {/* Success State */} + {isAuthenticated && ( + + +
+
+ +
+
+

Claude is ready to use!

+

+ {getAuthMethodLabel() && `Using ${getAuthMethodLabel()}. `}You can proceed to the next step +

+
+
+
+
+ )} + + {/* Navigation */} +
+ +
+ + +
+
+
+ ); +} + +// Codex Setup Step +function CodexSetupStep({ + onNext, + onBack, + onSkip, +}: { + onNext: () => void; + onBack: () => void; + onSkip: () => void; +}) { + const { + codexCliStatus, + codexAuthStatus, + codexInstallProgress, + setCodexCliStatus, + setCodexAuthStatus, + setCodexInstallProgress, + } = useSetupStore(); + const { setApiKeys, apiKeys } = useAppStore(); + + const [isChecking, setIsChecking] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); + const [showApiKeyInput, setShowApiKeyInput] = useState(false); + const [apiKey, setApiKey] = useState(""); + const [isSavingKey, setIsSavingKey] = useState(false); + + const checkStatus = useCallback(async () => { + console.log("[Codex Setup] Starting status check..."); + setIsChecking(true); + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + console.log("[Codex Setup] Setup API available:", !!setupApi); + console.log("[Codex Setup] getCodexStatus available:", !!setupApi?.getCodexStatus); + + if (setupApi?.getCodexStatus) { + const result = await setupApi.getCodexStatus(); + console.log("[Codex Setup] Raw status result:", result); + + if (result.success) { + const cliStatus = { + installed: result.status === "installed", + path: result.path || null, + version: result.version || null, + method: result.method || "none", + }; + console.log("[Codex Setup] CLI Status:", cliStatus); + setCodexCliStatus(cliStatus); + + if (result.auth) { + const authStatus = { + authenticated: result.auth.authenticated, + method: result.auth.method === "auth_file" ? "api_key" : result.auth.method === "env_var" ? "env" : "none", + apiKeyValid: result.auth.authenticated, + }; + console.log("[Codex Setup] Auth Status:", authStatus); + setCodexAuthStatus(authStatus as any); + } else { + console.log("[Codex Setup] No auth info in result"); + } + } else { + console.log("[Codex Setup] Status check failed:", result.error); + } + } else { + console.log("[Codex Setup] Setup API not available (web mode?)"); + } + } catch (error) { + console.error("[Codex Setup] Failed to check Codex status:", error); + } finally { + setIsChecking(false); + console.log("[Codex Setup] Status check complete"); + } + }, [setCodexCliStatus, setCodexAuthStatus]); + + useEffect(() => { + checkStatus(); + }, [checkStatus]); + + const handleInstall = async () => { + setIsInstalling(true); + setCodexInstallProgress({ + isInstalling: true, + currentStep: "Installing Codex CLI via npm...", + progress: 0, + output: [], + }); + + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + if (setupApi?.installCodex) { + const unsubscribe = setupApi.onInstallProgress?.((progress: { cli?: string; data?: string; type?: string }) => { + if (progress.cli === "codex") { + setCodexInstallProgress({ + output: [ + ...codexInstallProgress.output, + progress.data || progress.type || "", + ], + }); + } + }); + + const result = await setupApi.installCodex(); + + unsubscribe?.(); + + if (result.success) { + toast.success("Codex CLI installed successfully"); + await checkStatus(); + } else { + toast.error("Installation failed", { + description: result.error, + }); + } + } + } catch (error) { + console.error("Failed to install Codex:", error); + toast.error("Installation failed"); + } finally { + setIsInstalling(false); + setCodexInstallProgress({ isInstalling: false }); + } + }; + + const handleSaveApiKey = async () => { + console.log("[Codex Setup] Saving API key..."); + if (!apiKey.trim()) { + console.log("[Codex Setup] API key is empty"); + toast.error("Please enter an API key"); + return; + } + + setIsSavingKey(true); + try { + const api = getElectronAPI(); + const setupApi = api.setup; + + console.log("[Codex Setup] storeApiKey available:", !!setupApi?.storeApiKey); + + if (setupApi?.storeApiKey) { + console.log("[Codex Setup] Calling storeApiKey for openai..."); + const result = await setupApi.storeApiKey("openai", apiKey); + console.log("[Codex Setup] storeApiKey result:", result); + + if (result.success) { + console.log("[Codex Setup] API key stored successfully, updating state..."); + setApiKeys({ ...apiKeys, openai: apiKey }); + setCodexAuthStatus({ + authenticated: true, + method: "api_key", + apiKeyValid: true, + }); + toast.success("OpenAI API key saved"); + setShowApiKeyInput(false); + } else { + console.log("[Codex Setup] Failed to store API key:", result.error); + } + } else { + console.log("[Codex Setup] Web mode - storing API key in app state only"); + setApiKeys({ ...apiKeys, openai: apiKey }); + setCodexAuthStatus({ + authenticated: true, + method: "api_key", + apiKeyValid: true, + }); + toast.success("OpenAI API key saved"); + setShowApiKeyInput(false); + } + } catch (error) { + console.error("[Codex Setup] Failed to save API key:", error); + toast.error("Failed to save API key"); + } finally { + setIsSavingKey(false); + } + }; + + const copyCommand = (command: string) => { + navigator.clipboard.writeText(command); + toast.success("Command copied to clipboard"); + }; + + const isAuthenticated = codexAuthStatus?.authenticated || apiKeys.openai; + + const getAuthMethodLabel = () => { + if (!isAuthenticated) return null; + if (apiKeys.openai) return "API Key (Manual)"; + if (codexAuthStatus?.method === "api_key") return "API Key (Auth File)"; + if (codexAuthStatus?.method === "env") return "API Key (Environment)"; + if (codexAuthStatus?.method === "cli_verified") return "CLI Login (ChatGPT)"; + return "Authenticated"; + }; + + return ( +
+
+
+ +
+

+ Codex CLI Setup +

+

+ OpenAI's GPT-5.1 Codex for advanced code generation +

+
+ + {/* Status Card */} + + +
+ Installation Status + +
+
+ +
+ CLI Installation + {isChecking ? ( + + ) : codexCliStatus?.installed ? ( + + ) : ( + + )} +
+ + {codexCliStatus?.version && ( +
+ Version + + {codexCliStatus.version} + +
+ )} + +
+ Authentication + {isAuthenticated ? ( +
+ + {getAuthMethodLabel() && ( + + ({getAuthMethodLabel()}) + + )} +
+ ) : ( + + )} +
+
+
+ + {/* Installation Section */} + {!codexCliStatus?.installed && ( + + + + + Install Codex CLI + + + Install via npm (Node.js required) + + + +
+ +
+ + npm install -g @openai/codex + + +
+
+ + {codexInstallProgress.isInstalling && ( + + )} + +
+ +
+ +
+
+ +

+ Requires Node.js to be installed. If the auto-install fails, + try running the command manually in your terminal. +

+
+
+
+
+ )} + + {/* Authentication Section */} + {!isAuthenticated && ( + + + + + Authentication + + + Codex requires an OpenAI API key + + + + {codexCliStatus?.installed && ( +
+
+ +
+

+ Authenticate via CLI +

+

+ Run this command in your terminal: +

+
+ + codex auth login + + +
+
+
+
+ )} + +
+
+ +
+
+ + or enter API key + +
+
+ + {showApiKeyInput ? ( +
+
+ + setApiKey(e.target.value)} + className="bg-input border-border text-foreground" + data-testid="openai-api-key-input" + /> +

+ Get your API key from{" "} + + platform.openai.com + + +

+
+
+ + +
+
+ ) : ( + + )} +
+
+ )} + + {/* Success State */} + {isAuthenticated && ( + + +
+
+ +
+
+

+ Codex is ready to use! +

+

+ {getAuthMethodLabel() && `Authenticated via ${getAuthMethodLabel()}. `} + You can proceed to complete setup +

+
+
+
+
+ )} + + {/* Navigation */} +
+ +
+ + +
+
+
+ ); +} + +// Complete Step +function CompleteStep({ onFinish }: { onFinish: () => void }) { + const { claudeCliStatus, claudeAuthStatus, codexCliStatus, codexAuthStatus } = + useSetupStore(); + const { apiKeys } = useAppStore(); + + const claudeReady = + (claudeCliStatus?.installed && claudeAuthStatus?.authenticated) || + apiKeys.anthropic; + const codexReady = + (codexCliStatus?.installed && codexAuthStatus?.authenticated) || + apiKeys.openai; + + return ( +
+
+ +
+ +
+

+ Setup Complete! +

+

+ Your development environment is configured. You're ready to start + building with AI-powered assistance. +

+
+ +
+ + +
+ {claudeReady ? ( + + ) : ( + + )} +
+

Claude

+

+ {claudeReady ? "Ready to use" : "Configure later in settings"} +

+
+
+
+
+ + + +
+ {codexReady ? ( + + ) : ( + + )} +
+

Codex

+

+ {codexReady ? "Ready to use" : "Configure later in settings"} +

+
+
+
+
+
+ +
+
+ +
+

+ Your credentials are secure +

+

+ API keys are stored locally and never sent to our servers +

+
+
+
+ + +
+ ); +} + +// Main Setup View +export function SetupView() { + const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup, setSkipCodexSetup } = + useSetupStore(); + const { setCurrentView } = useAppStore(); + + const steps = ["welcome", "claude", "codex", "complete"] as const; + type StepName = typeof steps[number]; + const getStepName = (): StepName => { + if (currentStep === "claude_detect" || currentStep === "claude_auth") return "claude"; + if (currentStep === "codex_detect" || currentStep === "codex_auth") return "codex"; + if (currentStep === "welcome") return "welcome"; + return "complete"; + }; + const currentIndex = steps.indexOf(getStepName()); + + const handleNext = (from: string) => { + console.log("[Setup Flow] handleNext called from:", from, "currentStep:", currentStep); + switch (from) { + case "welcome": + console.log("[Setup Flow] Moving to claude_detect step"); + setCurrentStep("claude_detect"); + break; + case "claude": + console.log("[Setup Flow] Moving to codex_detect step"); + setCurrentStep("codex_detect"); + break; + case "codex": + console.log("[Setup Flow] Moving to complete step"); + setCurrentStep("complete"); + break; + } + }; + + const handleBack = (from: string) => { + console.log("[Setup Flow] handleBack called from:", from); + switch (from) { + case "claude": + setCurrentStep("welcome"); + break; + case "codex": + setCurrentStep("claude_detect"); + break; + } + }; + + const handleSkipClaude = () => { + console.log("[Setup Flow] Skipping Claude setup"); + setSkipClaudeSetup(true); + setCurrentStep("codex_detect"); + }; + + const handleSkipCodex = () => { + console.log("[Setup Flow] Skipping Codex setup"); + setSkipCodexSetup(true); + setCurrentStep("complete"); + }; + + const handleFinish = () => { + console.log("[Setup Flow] handleFinish called - completing setup"); + completeSetup(); + console.log("[Setup Flow] Setup completed, redirecting to welcome view"); + setCurrentView("welcome"); + }; + + return ( +
+ {/* Header */} +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Automaker + + Automaker Setup + +
+
+
+ + {/* Content */} +
+
+
+
+ +
+ +
+ {currentStep === "welcome" && ( + handleNext("welcome")} /> + )} + + {(currentStep === "claude_detect" || + currentStep === "claude_auth") && ( + handleNext("claude")} + onBack={() => handleBack("claude")} + onSkip={handleSkipClaude} + /> + )} + + {(currentStep === "codex_detect" || + currentStep === "codex_auth") && ( + handleNext("codex")} + onBack={() => handleBack("codex")} + onSkip={handleSkipCodex} + /> + )} + + {currentStep === "complete" && ( + + )} +
+
+
+
+
+ ); +} diff --git a/app/src/lib/electron.ts b/app/src/lib/electron.ts index 91392894..bf5c0c90 100644 --- a/app/src/lib/electron.ts +++ b/app/src/lib/electron.ts @@ -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 { diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index bb4d52f3..fe974c6f 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -4,6 +4,7 @@ import type { Project, TrashedProject } from "@/lib/electron"; export type ViewMode = | "welcome" + | "setup" | "spec" | "board" | "agent" diff --git a/app/src/store/setup-store.ts b/app/src/store/setup-store.ts new file mode 100644 index 00000000..6d4ded1d --- /dev/null +++ b/app/src/store/setup-store.ts @@ -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) => void; + resetClaudeInstallProgress: () => void; + + // Codex CLI + setCodexCliStatus: (status: CliStatus | null) => void; + setCodexAuthStatus: (status: CodexAuthStatus | null) => void; + setCodexInstallProgress: (progress: Partial) => 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()( + 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, + }), + } + ) +); diff --git a/app/tests/utils.ts b/app/tests/utils.ts index 9168e372..d08a8b01 100644 --- a/app/tests/utils.ts +++ b/app/tests/utils.ts @@ -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 { + 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 { + 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 { + 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 { + return waitForElement(page, "setup-view", { timeout: 10000 }); +} + +/** + * Click "Get Started" button on setup welcome step + */ +export async function clickSetupGetStarted(page: Page): Promise { + const button = await getByTestId(page, "setup-start-button"); + await button.click(); +} + +/** + * Click continue on Claude setup step + */ +export async function clickClaudeContinue(page: Page): Promise { + const button = await getByTestId(page, "claude-next-button"); + await button.click(); +} + +/** + * Click continue on Codex setup step + */ +export async function clickCodexContinue(page: Page): Promise { + const button = await getByTestId(page, "codex-next-button"); + await button.click(); +} + +/** + * Click finish on setup complete step + */ +export async function clickSetupFinish(page: Page): Promise { + 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 { + // 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 { + // 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(); +} From e86ef96a5ffc030ad9b4a5287f135d5bab024b22 Mon Sep 17 00:00:00 2001 From: Web Dev Cody Date: Wed, 10 Dec 2025 13:24:48 -0500 Subject: [PATCH 03/10] Enhance README with setup instructions Added detailed setup instructions for the project, including cloning the repository, installing dependencies, running setup commands, and starting the development server. --- app/README.md | 67 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/app/README.md b/app/README.md index fc43a642..d23a9d80 100644 --- a/app/README.md +++ b/app/README.md @@ -5,6 +5,7 @@ Automaker is an autonomous AI development studio that helps you build software f --- > **[!CAUTION]** +> > ## Security Disclaimer > > **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.** @@ -17,6 +18,56 @@ Automaker is an autonomous AI development studio that helps you build software f --- +## Getting Started + +**Step 1:** Clone this repository: + +```bash +git clone git@github.com:AutoMaker-Org/automaker.git +cd automaker +``` + +**Step 2:** Install dependencies: + +```bash +npm install +``` + +**Step 3:** Run the Claude Code setup token command: + +```bash +claude setup-token +``` + +> **โš ๏ธ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching. + +**Step 4:** Export the Claude Code OAuth token in your shell: + +```bash +export CLAUDE_CODE_OAUTH_TOKEN="your-token-here" +``` + +**Step 5:** Start the development server: + +```bash +npm run dev:electron +``` + +This will start both the Next.js development server and the Electron application. + +**Step 6:** MOST IMPORANT: Run the Following after all is setup + +```bash +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +``` + ## Features - ๐Ÿ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages @@ -37,22 +88,6 @@ Automaker is an autonomous AI development studio that helps you build software f - ๐ŸŽจ **Dark/Light Theme** - Beautiful UI with theme support - ๐Ÿ–ฅ๏ธ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3007](http://localhost:3007) with your browser to see the result. - ## Tech Stack - [Next.js](https://nextjs.org) - React framework From 13f68cba4c6ee387bddf6760aed1a7dab1a30937 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:19:41 +0000 Subject: [PATCH 04/10] Initial plan From e5095c79110756c592dd96b975b39b613c77a07b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:31:31 +0000 Subject: [PATCH 05/10] Add keyboard shortcuts store and update components to use customizable shortcuts Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/layout/sidebar.tsx | 65 ++++++++++--------- app/src/components/session-manager.tsx | 8 ++- app/src/components/views/board-view.tsx | 19 +++--- app/src/hooks/use-keyboard-shortcuts.ts | 39 ++---------- app/src/store/app-store.ts | 85 +++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 75 deletions(-) diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 77e92c28..15046e98 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -64,9 +64,7 @@ import { import { Button } from "@/components/ui/button"; import { useKeyboardShortcuts, - NAV_SHORTCUTS, - UI_SHORTCUTS, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { getElectronAPI, Project, TrashedProject } from "@/lib/electron"; @@ -221,6 +219,9 @@ export function Sidebar() { theme: globalTheme, } = useAppStore(); + // Get customizable keyboard shortcuts + const shortcuts = useKeyboardShortcutsConfig(); + // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); const [showTrashDialog, setShowTrashDialog] = useState(false); @@ -496,13 +497,13 @@ export function Sidebar() { id: "board", label: "Kanban Board", icon: LayoutGrid, - shortcut: NAV_SHORTCUTS.board, + shortcut: shortcuts.board, }, { id: "agent", label: "Agent Runner", icon: Bot, - shortcut: NAV_SHORTCUTS.agent, + shortcut: shortcuts.agent, }, ], }, @@ -513,25 +514,25 @@ export function Sidebar() { id: "spec", label: "Spec Editor", icon: FileText, - shortcut: NAV_SHORTCUTS.spec, + shortcut: shortcuts.spec, }, { id: "context", label: "Context", icon: BookOpen, - shortcut: NAV_SHORTCUTS.context, + shortcut: shortcuts.context, }, { id: "tools", label: "Agent Tools", icon: Wrench, - shortcut: NAV_SHORTCUTS.tools, + shortcut: shortcuts.tools, }, { id: "profiles", label: "AI Profiles", icon: UserCircle, - shortcut: NAV_SHORTCUTS.profiles, + shortcut: shortcuts.profiles, }, ], }, @@ -573,26 +574,26 @@ export function Sidebar() { // Build keyboard shortcuts for navigation const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // Sidebar toggle shortcut - always available - shortcuts.push({ - key: UI_SHORTCUTS.toggleSidebar, + shortcutsList.push({ + key: shortcuts.toggleSidebar, action: () => toggleSidebar(), description: "Toggle sidebar", }); // Open project shortcut - opens the folder selection dialog directly - shortcuts.push({ - key: ACTION_SHORTCUTS.openProject, + shortcutsList.push({ + key: shortcuts.openProject, action: () => handleOpenFolder(), description: "Open folder selection dialog", }); // Project picker shortcut - only when we have projects if (projects.length > 0) { - shortcuts.push({ - key: ACTION_SHORTCUTS.projectPicker, + shortcutsList.push({ + key: shortcuts.projectPicker, action: () => setIsProjectPickerOpen((prev) => !prev), description: "Toggle project picker", }); @@ -600,13 +601,13 @@ export function Sidebar() { // Project cycling shortcuts - only when we have project history if (projectHistory.length > 1) { - shortcuts.push({ - key: ACTION_SHORTCUTS.cyclePrevProject, + shortcutsList.push({ + key: shortcuts.cyclePrevProject, action: () => cyclePrevProject(), description: "Cycle to previous project (MRU)", }); - shortcuts.push({ - key: ACTION_SHORTCUTS.cycleNextProject, + shortcutsList.push({ + key: shortcuts.cycleNextProject, action: () => cycleNextProject(), description: "Cycle to next project (LRU)", }); @@ -617,7 +618,7 @@ export function Sidebar() { navSections.forEach((section) => { section.items.forEach((item) => { if (item.shortcut) { - shortcuts.push({ + shortcutsList.push({ key: item.shortcut, action: () => setCurrentView(item.id as any), description: `Navigate to ${item.label}`, @@ -627,15 +628,16 @@ export function Sidebar() { }); // Add settings shortcut - shortcuts.push({ - key: NAV_SHORTCUTS.settings, + shortcutsList.push({ + key: shortcuts.settings, action: () => setCurrentView("settings"), description: "Navigate to Settings", }); } - return shortcuts; + return shortcutsList; }, [ + shortcuts, currentProject, setCurrentView, toggleSidebar, @@ -644,6 +646,7 @@ export function Sidebar() { projectHistory.length, cyclePrevProject, cycleNextProject, + navSections, ]); // Register keyboard shortcuts @@ -682,7 +685,7 @@ export function Sidebar() { className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70" data-testid="sidebar-toggle-shortcut" > - {UI_SHORTCUTS.toggleSidebar} + {shortcuts.toggleSidebar}
@@ -735,12 +738,12 @@ export function Sidebar() { )} diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 84fcf2be..ab504986 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -86,7 +86,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { useAutoMode } from "@/hooks/use-auto-mode"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { useWindowState } from "@/hooks/use-window-state"; @@ -189,6 +189,7 @@ export function BoardView() { showProfilesOnly, aiProfiles, } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [activeFeature, setActiveFeature] = useState(null); const [editingFeature, setEditingFeature] = useState(null); const [showAddDialog, setShowAddDialog] = useState(false); @@ -292,14 +293,14 @@ export function BoardView() { // Keyboard shortcuts for this view const boardShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = [ + const shortcutsList: KeyboardShortcut[] = [ { - key: ACTION_SHORTCUTS.addFeature, + key: shortcuts.addFeature, action: () => setShowAddDialog(true), description: "Add new feature", }, { - key: ACTION_SHORTCUTS.startNext, + key: shortcuts.startNext, action: () => startNextFeaturesRef.current(), description: "Start next features from backlog", }, @@ -309,7 +310,7 @@ export function BoardView() { inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => { // Keys 1-9 for first 9 cards, 0 for 10th card const key = index === 9 ? "0" : String(index + 1); - shortcuts.push({ + shortcutsList.push({ key, action: () => { setOutputFeature(feature); @@ -319,8 +320,8 @@ export function BoardView() { }); }); - return shortcuts; - }, [inProgressFeaturesForShortcuts]); + return shortcutsList; + }, [inProgressFeaturesForShortcuts, shortcuts]); useKeyboardShortcuts(boardShortcuts); // Prevent hydration issues @@ -1567,7 +1568,7 @@ export function BoardView() { className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground inline-flex items-center justify-center" data-testid="shortcut-add-feature" > - {ACTION_SHORTCUTS.addFeature} + {shortcuts.addFeature} @@ -1636,7 +1637,7 @@ export function BoardView() { Start Next - {ACTION_SHORTCUTS.startNext} + {shortcuts.startNext} )} diff --git a/app/src/hooks/use-keyboard-shortcuts.ts b/app/src/hooks/use-keyboard-shortcuts.ts index 9622d10f..a9b901ff 100644 --- a/app/src/hooks/use-keyboard-shortcuts.ts +++ b/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useCallback } from "react"; +import { useAppStore } from "@/store/app-store"; export interface KeyboardShortcut { key: string; @@ -97,36 +98,10 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { } /** - * Shortcut definitions for navigation + * Hook to get current keyboard shortcuts from store + * This replaces the static constants and allows customization */ -export const NAV_SHORTCUTS: Record = { - board: "K", // K for Kanban - agent: "A", // A for Agent - spec: "D", // D for Document (Spec) - context: "C", // C for Context - tools: "T", // T for Tools - settings: "S", // S for Settings - profiles: "M", // M for Models/profiles -}; - -/** - * Shortcut definitions for UI controls - */ -export const UI_SHORTCUTS: Record = { - toggleSidebar: "`", // Backtick to toggle sidebar -}; - -/** - * Shortcut definitions for add buttons - */ -export const ACTION_SHORTCUTS: Record = { - addFeature: "N", // N for New feature - addContextFile: "F", // F for File (add context file) - startNext: "G", // G for Grab (start next features from backlog) - newSession: "N", // N for New session (in agent view) - openProject: "O", // O for Open project (navigate to welcome view) - projectPicker: "P", // P for Project picker - cyclePrevProject: "Q", // Q for previous project (cycle back through MRU) - cycleNextProject: "E", // E for next project (cycle forward through MRU) - addProfile: "N", // N for New profile (when in profiles view) -}; +export function useKeyboardShortcutsConfig() { + const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts); + return keyboardShortcuts; +} diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 738de87f..77779ecb 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -37,6 +37,58 @@ export interface ApiKeys { openai: string; } +// Keyboard Shortcuts +export interface KeyboardShortcuts { + // Navigation shortcuts + board: string; + agent: string; + spec: string; + context: string; + tools: string; + settings: string; + profiles: string; + + // UI shortcuts + toggleSidebar: string; + + // Action shortcuts + addFeature: string; + addContextFile: string; + startNext: string; + newSession: string; + openProject: string; + projectPicker: string; + cyclePrevProject: string; + cycleNextProject: string; + addProfile: string; +} + +// Default keyboard shortcuts +export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { + // Navigation + board: "K", + agent: "A", + spec: "D", + context: "C", + tools: "T", + settings: "S", + profiles: "M", + + // UI + toggleSidebar: "`", + + // Actions + addFeature: "N", + addContextFile: "F", + startNext: "G", + newSession: "N", + openProject: "O", + projectPicker: "P", + cyclePrevProject: "Q", + cycleNextProject: "E", + addProfile: "N", +}; + export interface ImageAttachment { id: string; data: string; // base64 encoded image data @@ -203,6 +255,9 @@ export interface AppState { // Profile Display Settings showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection + // Keyboard Shortcuts + keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -303,6 +358,11 @@ export interface AppActions { // Profile Display Settings actions setShowProfilesOnly: (enabled: boolean) => void; + // Keyboard Shortcuts actions + setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void; + setKeyboardShortcuts: (shortcuts: Partial) => void; + resetKeyboardShortcuts: () => void; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -404,6 +464,7 @@ const initialState: AppState = { defaultSkipTests: false, // Default to TDD mode (tests enabled) useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) + keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -907,6 +968,29 @@ export const useAppStore = create()( // Profile Display Settings actions setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }), + // Keyboard Shortcuts actions + setKeyboardShortcut: (key, value) => { + set({ + keyboardShortcuts: { + ...get().keyboardShortcuts, + [key]: value, + }, + }); + }, + + setKeyboardShortcuts: (shortcuts) => { + set({ + keyboardShortcuts: { + ...get().keyboardShortcuts, + ...shortcuts, + }, + }); + }, + + resetKeyboardShortcuts: () => { + set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }); + }, + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random() @@ -985,6 +1069,7 @@ export const useAppStore = create()( defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, + keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, lastSelectedSessionByProject: state.lastSelectedSessionByProject, }), From 3a4597028072f28f1a202f083131dc0a3ba0a981 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:34:42 +0000 Subject: [PATCH 06/10] Add keyboard shortcuts customization UI to settings view Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/settings-view.tsx | 397 ++++++++++++++++++++- 1 file changed, 396 insertions(+), 1 deletion(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 57f2c839..e399c09f 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1,7 +1,8 @@ "use client"; import { useState, useEffect, useRef, useCallback } from "react"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS } from "@/store/app-store"; +import type { KeyboardShortcuts } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -58,6 +59,7 @@ const NAV_ITEMS = [ { id: "codex", label: "Codex", icon: Atom }, { id: "appearance", label: "Appearance", icon: Palette }, { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, + { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, { id: "danger", label: "Danger Zone", icon: Trash2 }, ]; @@ -79,6 +81,9 @@ export function SettingsView() { setShowProfilesOnly, currentProject, moveProjectToTrash, + keyboardShortcuts, + setKeyboardShortcut, + resetKeyboardShortcuts, } = useAppStore(); const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); const [googleKey, setGoogleKey] = useState(apiKeys.google); @@ -137,6 +142,9 @@ export function SettingsView() { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); + const [editingShortcut, setEditingShortcut] = useState(null); + const [shortcutValue, setShortcutValue] = useState(""); + const [shortcutError, setShortcutError] = useState(null); const scrollContainerRef = useRef(null); useEffect(() => { @@ -1334,6 +1342,393 @@ export function SettingsView() { + {/* Keyboard Shortcuts Section */} +
+
+
+ +

+ Keyboard Shortcuts +

+
+

+ Customize keyboard shortcuts for navigation and actions. Click + on any shortcut to edit it. +

+
+
+ {/* Navigation Shortcuts */} +
+
+

+ Navigation +

+ +
+
+ {[ + { key: "board" as keyof KeyboardShortcuts, label: "Kanban Board" }, + { key: "agent" as keyof KeyboardShortcuts, label: "Agent Runner" }, + { key: "spec" as keyof KeyboardShortcuts, label: "Spec Editor" }, + { key: "context" as keyof KeyboardShortcuts, label: "Context" }, + { key: "tools" as keyof KeyboardShortcuts, label: "Agent Tools" }, + { key: "profiles" as keyof KeyboardShortcuts, label: "AI Profiles" }, + { key: "settings" as keyof KeyboardShortcuts, label: "Settings" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value.toUpperCase(); + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v.toUpperCase() === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+ {shortcutError && ( +

{shortcutError}

+ )} +
+ + {/* UI Shortcuts */} +
+

+ UI Controls +

+
+ {[ + { key: "toggleSidebar" as keyof KeyboardShortcuts, label: "Toggle Sidebar" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value; + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+
+ + {/* Action Shortcuts */} +
+

+ Actions +

+
+ {[ + { key: "addFeature" as keyof KeyboardShortcuts, label: "Add Feature" }, + { key: "addContextFile" as keyof KeyboardShortcuts, label: "Add Context File" }, + { key: "startNext" as keyof KeyboardShortcuts, label: "Start Next Features" }, + { key: "newSession" as keyof KeyboardShortcuts, label: "New Session" }, + { key: "openProject" as keyof KeyboardShortcuts, label: "Open Project" }, + { key: "projectPicker" as keyof KeyboardShortcuts, label: "Project Picker" }, + { key: "cyclePrevProject" as keyof KeyboardShortcuts, label: "Previous Project" }, + { key: "cycleNextProject" as keyof KeyboardShortcuts, label: "Next Project" }, + { key: "addProfile" as keyof KeyboardShortcuts, label: "Add Profile" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value.toUpperCase(); + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v.toUpperCase() === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+
+ + {/* Information */} +
+ +
+

+ About Keyboard Shortcuts +

+

+ Shortcuts won't trigger when typing in input fields. Use + single keys (A-Z, 0-9) or special keys like ` (backtick). + Changes take effect immediately. +

+
+
+
+
+ {/* Feature Defaults Section */}
Date: Wed, 10 Dec 2025 17:38:45 +0000 Subject: [PATCH 07/10] Update remaining components to use customizable keyboard shortcuts Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/agent-view.tsx | 13 +++++++------ app/src/components/views/context-view.tsx | 9 +++++---- app/src/components/views/profiles-view.tsx | 15 ++++++++------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/components/views/agent-view.tsx b/app/src/components/views/agent-view.tsx index 7e59a123..2d6a7b7c 100644 --- a/app/src/components/views/agent-view.tsx +++ b/app/src/components/views/agent-view.tsx @@ -26,12 +26,13 @@ import { Markdown } from "@/components/ui/markdown"; import type { ImageAttachment } from "@/store/app-store"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; export function AgentView() { const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [input, setInput] = useState(""); const [selectedImages, setSelectedImages] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); @@ -417,12 +418,12 @@ export function AgentView() { // Keyboard shortcuts for agent view const agentShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // New session shortcut - only when in agent view with a project if (currentProject) { - shortcuts.push({ - key: ACTION_SHORTCUTS.newSession, + shortcutsList.push({ + key: shortcuts.newSession, action: () => { if (quickCreateSessionRef.current) { quickCreateSessionRef.current(); @@ -432,8 +433,8 @@ export function AgentView() { }); } - return shortcuts; - }, [currentProject]); + return shortcutsList; + }, [currentProject, shortcuts]); // Register keyboard shortcuts useKeyboardShortcuts(agentShortcuts); diff --git a/app/src/components/views/context-view.tsx b/app/src/components/views/context-view.tsx index 5cb184a9..b8d1a1ec 100644 --- a/app/src/components/views/context-view.tsx +++ b/app/src/components/views/context-view.tsx @@ -19,7 +19,7 @@ import { } from "lucide-react"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { @@ -43,6 +43,7 @@ interface ContextFile { export function ContextView() { const { currentProject } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [contextFiles, setContextFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -63,12 +64,12 @@ export function ContextView() { const contextShortcuts: KeyboardShortcut[] = useMemo( () => [ { - key: ACTION_SHORTCUTS.addContextFile, + key: shortcuts.addContextFile, action: () => setIsAddDialogOpen(true), description: "Add new context file", }, ], - [] + [shortcuts] ); useKeyboardShortcuts(contextShortcuts); @@ -374,7 +375,7 @@ export function ContextView() { className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-secondary border border-border" data-testid="shortcut-add-context-file" > - {ACTION_SHORTCUTS.addContextFile} + {shortcuts.addContextFile}
diff --git a/app/src/components/views/profiles-view.tsx b/app/src/components/views/profiles-view.tsx index 82bf811d..bd882845 100644 --- a/app/src/components/views/profiles-view.tsx +++ b/app/src/components/views/profiles-view.tsx @@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea"; import { cn, modelSupportsThinking } from "@/lib/utils"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { @@ -440,6 +440,7 @@ function ProfileForm({ export function ProfilesView() { const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [showAddDialog, setShowAddDialog] = useState(false); const [editingProfile, setEditingProfile] = useState(null); @@ -508,17 +509,17 @@ export function ProfilesView() { // Build keyboard shortcuts for profiles view const profilesShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // Add profile shortcut - when in profiles view - shortcuts.push({ - key: ACTION_SHORTCUTS.addProfile, + shortcutsList.push({ + key: shortcuts.addProfile, action: () => setShowAddDialog(true), description: "Create new profile", }); - return shortcuts; - }, []); + return shortcutsList; + }, [shortcuts]); // Register keyboard shortcuts for profiles view useKeyboardShortcuts(profilesShortcuts); @@ -549,7 +550,7 @@ export function ProfilesView() { New Profile - {ACTION_SHORTCUTS.addProfile} + {shortcuts.addProfile} From 8095a3aef72655a3cda0f17a361b0daa60fe7e41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:41:23 +0000 Subject: [PATCH 08/10] Fix keyboard shortcuts: add comments for duplicate keys and increase maxLength for special characters Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/settings-view.tsx | 12 ++++++------ app/src/store/app-store.ts | 20 +++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index e399c09f..9ec2b447 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1422,9 +1422,9 @@ export function SettingsView() { setShortcutError(null); } }} - className="w-20 h-8 text-center font-mono" + className="w-24 h-8 text-center font-mono" placeholder="Key" - maxLength={1} + maxLength={2} autoFocus data-testid={`edit-shortcut-${key}`} /> @@ -1534,9 +1534,9 @@ export function SettingsView() { setShortcutError(null); } }} - className="w-20 h-8 text-center font-mono" + className="w-24 h-8 text-center font-mono" placeholder="Key" - maxLength={1} + maxLength={2} autoFocus data-testid={`edit-shortcut-${key}`} /> @@ -1649,9 +1649,9 @@ export function SettingsView() { setShortcutError(null); } }} - className="w-20 h-8 text-center font-mono" + className="w-24 h-8 text-center font-mono" placeholder="Key" - maxLength={1} + maxLength={2} autoFocus data-testid={`edit-shortcut-${key}`} /> diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 77779ecb..7c9ced56 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -78,15 +78,17 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { toggleSidebar: "`", // Actions - addFeature: "N", - addContextFile: "F", - startNext: "G", - newSession: "N", - openProject: "O", - projectPicker: "P", - cyclePrevProject: "Q", - cycleNextProject: "E", - addProfile: "N", + // Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile) + // This is intentional as they are context-specific and only active in their respective views + addFeature: "N", // Only active in board view + addContextFile: "F", // Only active in context view + startNext: "G", // Only active in board view + newSession: "N", // Only active in agent view + openProject: "O", // Global shortcut + projectPicker: "P", // Global shortcut + cyclePrevProject: "Q", // Global shortcut + cycleNextProject: "E", // Global shortcut + addProfile: "N", // Only active in profiles view }; export interface ImageAttachment { From 1edb7f6b6247bb27d232f9714a9c03c89ae85f64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:42:56 +0000 Subject: [PATCH 09/10] Add missing RotateCcw import in settings view Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/settings-view.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 9ec2b447..36175bcf 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -40,6 +40,7 @@ import { TestTube, Settings2, RefreshCw, + RotateCcw, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { Checkbox } from "@/components/ui/checkbox"; From 888478b40378eebdf99431958643c7521e5746e3 Mon Sep 17 00:00:00 2001 From: Kacper Date: Wed, 10 Dec 2025 19:47:01 +0100 Subject: [PATCH 10/10] chore: copy readme to workspace root --- README.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..d23a9d80 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# Automaker + +Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion. + +--- + +> **[!CAUTION]** +> +> ## Security Disclaimer +> +> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.** +> +> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it. +> +> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine. +> +> **[Read the full disclaimer](../DISCLAIMER.md)** + +--- + +## Getting Started + +**Step 1:** Clone this repository: + +```bash +git clone git@github.com:AutoMaker-Org/automaker.git +cd automaker +``` + +**Step 2:** Install dependencies: + +```bash +npm install +``` + +**Step 3:** Run the Claude Code setup token command: + +```bash +claude setup-token +``` + +> **โš ๏ธ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching. + +**Step 4:** Export the Claude Code OAuth token in your shell: + +```bash +export CLAUDE_CODE_OAUTH_TOKEN="your-token-here" +``` + +**Step 5:** Start the development server: + +```bash +npm run dev:electron +``` + +This will start both the Next.js development server and the Electron application. + +**Step 6:** MOST IMPORANT: Run the Following after all is setup + +```bash +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +``` + +## Features + +- ๐Ÿ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages +- ๐Ÿค– **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress" +- ๐Ÿง  **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more +- ๐Ÿ’ญ **Extended Thinking** - Enable extended thinking modes for complex problem-solving +- ๐Ÿ“ก **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented +- ๐Ÿ” **Project Analysis** - AI-powered project structure analysis to understand your codebase +- ๐Ÿ“ **Context Management** - Add context files to help AI agents understand your project better +- ๐Ÿ’ก **Feature Suggestions** - AI-generated feature suggestions based on your project +- ๐Ÿ–ผ๏ธ **Image Support** - Attach images and screenshots to feature descriptions +- โšก **Concurrent Processing** - Configure concurrency to process multiple features simultaneously +- ๐Ÿงช **Test Integration** - Automatic test running and verification for implemented features +- ๐Ÿ”€ **Git Integration** - View git diffs and track changes made by AI agents +- ๐Ÿ‘ค **AI Profiles** - Create and manage different AI agent profiles for various tasks +- ๐Ÿ’ฌ **Chat History** - Keep track of conversations and interactions with AI agents +- โŒจ๏ธ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts +- ๐ŸŽจ **Dark/Light Theme** - Beautiful UI with theme support +- ๐Ÿ–ฅ๏ธ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux + +## Tech Stack + +- [Next.js](https://nextjs.org) - React framework +- [Electron](https://www.electronjs.org/) - Desktop application framework +- [Tailwind CSS](https://tailwindcss.com/) - Styling +- [Zustand](https://zustand-demo.pmnd.rs/) - State management +- [dnd-kit](https://dndkit.com/) - Drag and drop functionality + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +## License + +See [LICENSE](../LICENSE) for details.