diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 00000000..7dab6afe --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,33 @@ +name: PR Build Check + +on: + pull_request: + branches: + - "*" + push: + branches: + - main + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: app/package-lock.json + + - name: Install dependencies + working-directory: ./app + run: npm ci + + - name: Run build:electron + working-directory: ./app + run: npm run build:electron diff --git a/app/src/components/views/analysis-view.tsx b/app/src/components/views/analysis-view.tsx index 4a6d7237..1fb2852d 100644 --- a/app/src/components/views/analysis-view.tsx +++ b/app/src/components/views/analysis-view.tsx @@ -1,7 +1,12 @@ "use client"; import { useCallback, useState } from "react"; -import { useAppStore, FileTreeNode, ProjectAnalysis } from "@/store/app-store"; +import { + useAppStore, + FileTreeNode, + ProjectAnalysis, + Feature, +} from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { Card, @@ -763,7 +768,17 @@ ${Object.entries(projectAnalysis.filesByExtension) throw new Error("Features API not available"); } - for (const feature of detectedFeatures) { + // Convert DetectedFeature to Feature by adding required id and status + for (const detectedFeature of detectedFeatures) { + const feature: Feature = { + id: `feature-${Date.now()}-${Math.random() + .toString(36) + .substr(2, 9)}`, + category: detectedFeature.category, + description: detectedFeature.description, + steps: detectedFeature.steps, + status: "backlog" as const, + }; await api.features.create(currentProject.path, feature); } diff --git a/app/src/components/views/interview-view.tsx b/app/src/components/views/interview-view.tsx index 144be421..354858b8 100644 --- a/app/src/components/views/interview-view.tsx +++ b/app/src/components/views/interview-view.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useCallback, useRef, useEffect } from "react"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, Feature } from "@/store/app-store"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -313,11 +313,11 @@ export function InterviewView() { ); // Create initial feature in the features folder - const initialFeature = { + const initialFeature: Feature = { id: `feature-${Date.now()}-0`, category: "Core", description: "Initial project setup", - status: "backlog", + status: "backlog" as const, steps: [ "Step 1: Review app_spec.txt", "Step 2: Set up development environment", @@ -325,6 +325,9 @@ export function InterviewView() { ], skipTests: true, }; + if (!api.features) { + throw new Error("Features API not available"); + } await api.features.create(fullProjectPath, initialFeature); const project = { diff --git a/app/src/components/views/settings-view/components/delete-project-dialog.tsx b/app/src/components/views/settings-view/components/delete-project-dialog.tsx index 0ac5870b..c60ce76f 100644 --- a/app/src/components/views/settings-view/components/delete-project-dialog.tsx +++ b/app/src/components/views/settings-view/components/delete-project-dialog.tsx @@ -8,7 +8,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import type { Project } from "@/store/app-store"; +import type { Project } from "@/lib/electron"; interface DeleteProjectDialogProps { open: boolean; @@ -49,14 +49,19 @@ export function DeleteProjectDialog({
-

{project.name}

-

{project.path}

+

+ {project.name} +

+

+ {project.path} +

)}

- The folder will remain on disk until you permanently delete it from Trash. + The folder will remain on disk until you permanently delete it from + Trash.

diff --git a/app/src/components/views/settings-view/components/settings-navigation.tsx b/app/src/components/views/settings-view/components/settings-navigation.tsx index 7a90b7ca..b8c362c5 100644 --- a/app/src/components/views/settings-view/components/settings-navigation.tsx +++ b/app/src/components/views/settings-view/components/settings-navigation.tsx @@ -1,5 +1,5 @@ import { cn } from "@/lib/utils"; -import type { Project } from "@/store/app-store"; +import type { Project } from "@/lib/electron"; import type { NavigationItem } from "../config/navigation"; interface SettingsNavigationProps { diff --git a/app/src/components/views/setup-view.tsx b/app/src/components/views/setup-view.tsx index cac5e1f1..2c758b7c 100644 --- a/app/src/components/views/setup-view.tsx +++ b/app/src/components/views/setup-view.tsx @@ -61,7 +61,12 @@ function StatusBadge({ status, label, }: { - status: "installed" | "not_installed" | "checking" | "authenticated" | "not_authenticated"; + status: + | "installed" + | "not_installed" + | "checking" + | "authenticated" + | "not_authenticated"; label: string; }) { const getStatusConfig = () => { @@ -128,8 +133,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) { Welcome to Automaker

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

@@ -143,7 +148,8 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {

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

@@ -200,7 +206,9 @@ function ClaudeSetupStep({ const [isChecking, setIsChecking] = useState(false); const [isInstalling, setIsInstalling] = useState(false); - const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>(null); + const [authMethod, setAuthMethod] = useState<"token" | "api_key" | null>( + null + ); const [oauthToken, setOAuthToken] = useState(""); const [apiKey, setApiKey] = useState(""); const [isSaving, setIsSaving] = useState(false); @@ -213,9 +221,18 @@ function ClaudeSetupStep({ 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] 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) { @@ -224,7 +241,7 @@ function ClaudeSetupStep({ if (result.success) { const cliStatus = { - installed: result.installed || result.status === "installed", + installed: result.status === "installed", path: result.path || null, version: result.version || null, method: result.method || "none", @@ -235,14 +252,16 @@ function ClaudeSetupStep({ 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", + 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, + apiKeyValid: + result.auth.hasStoredApiKey || result.auth.hasEnvApiKey, }; console.log("[Claude Setup] Auth Status:", authStatus); setClaudeAuthStatus(authStatus as any); @@ -274,13 +293,18 @@ function ClaudeSetupStep({ 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 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?.(); @@ -290,17 +314,17 @@ function ClaudeSetupStep({ // 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)); - + 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)); - + 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) { @@ -308,18 +332,21 @@ function ClaudeSetupStep({ 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))); + 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.", + 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, }); } @@ -349,7 +376,10 @@ function ClaudeSetupStep({ const setupApi = api.setup; if (setupApi?.storeApiKey) { - const result = await setupApi.storeApiKey("anthropic_oauth_token", oauthToken); + const result = await setupApi.storeApiKey( + "anthropic_oauth_token", + oauthToken + ); console.log("[Claude Setup] Store OAuth token result:", result); if (result.success) { @@ -434,7 +464,8 @@ function ClaudeSetupStep({ const getAuthMethodLabel = () => { if (!isAuthenticated) return null; if (claudeAuthStatus?.method === "oauth") return "Subscription Token"; - if (apiKeys.anthropic || claudeAuthStatus?.method === "api_key") return "API Key"; + if (apiKeys.anthropic || claudeAuthStatus?.method === "api_key") + return "API Key"; return "Authenticated"; }; @@ -457,8 +488,15 @@ function ClaudeSetupStep({
Status -
@@ -477,7 +515,9 @@ function ClaudeSetupStep({ {claudeCliStatus?.version && (
Version - {claudeCliStatus.version} + + {claudeCliStatus.version} +
)} @@ -487,11 +527,16 @@ function ClaudeSetupStep({
{getAuthMethodLabel() && ( - ({getAuthMethodLabel()}) + + ({getAuthMethodLabel()}) + )}
) : ( - + )} @@ -505,16 +550,28 @@ function ClaudeSetupStep({ Install Claude CLI - Required for subscription-based authentication + + Required for subscription-based authentication +
- +
curl -fsSL https://claude.ai/install.sh | bash -
@@ -526,13 +583,21 @@ function ClaudeSetupStep({ irm https://claude.ai/install.ps1 | iex -
- {claudeInstallProgress.isInstalling && } + {claudeInstallProgress.isInstalling && ( + + )}
- +
-
@@ -620,7 +705,8 @@ function ClaudeSetupStep({

- Install Claude CLI first to use subscription authentication + Install Claude CLI first to use subscription + authentication

)} @@ -634,10 +720,17 @@ function ClaudeSetupStep({

API Key

-

Pay-per-use with your Anthropic API key

+

+ Pay-per-use with your Anthropic API key +

- +
-
@@ -688,9 +789,15 @@ function ClaudeSetupStep({
-

Subscription

-

Use your Claude subscription

-

No API charges

+

+ Subscription +

+

+ Use your Claude subscription +

+

+ No API charges +

@@ -704,7 +811,9 @@ function ClaudeSetupStep({

API Key

-

Use Anthropic API key

+

+ Use Anthropic API key +

Pay-per-use

@@ -724,9 +833,12 @@ function ClaudeSetupStep({
-

Claude is ready to use!

+

+ Claude is ready to use! +

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

@@ -736,15 +848,27 @@ function ClaudeSetupStep({ {/* Navigation */}
-
- - @@ -804,7 +928,10 @@ function CodexSetupStep({ const setupApi = api.setup; console.log("[Codex Setup] Setup API available:", !!setupApi); - console.log("[Codex Setup] getCodexStatus available:", !!setupApi?.getCodexStatus); + console.log( + "[Codex Setup] getCodexStatus available:", + !!setupApi?.getCodexStatus + ); if (setupApi?.getCodexStatus) { const result = await setupApi.getCodexStatus(); @@ -822,12 +949,15 @@ function CodexSetupStep({ if (result.auth) { const method = mapAuthMethod(result.auth.method); - + const authStatus: CodexAuthStatus = { authenticated: result.auth.authenticated, method, // Only set apiKeyValid for actual API key methods, not CLI login - apiKeyValid: method === "cli_verified" || method === "cli_tokens" ? undefined : result.auth.authenticated, + apiKeyValid: + method === "cli_verified" || method === "cli_tokens" + ? undefined + : result.auth.authenticated, }; console.log("[Codex Setup] Auth Status:", authStatus); setCodexAuthStatus(authStatus); @@ -866,16 +996,18 @@ function CodexSetupStep({ 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 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(); @@ -912,7 +1044,10 @@ function CodexSetupStep({ const api = getElectronAPI(); const setupApi = api.setup; - console.log("[Codex Setup] storeApiKey available:", !!setupApi?.storeApiKey); + console.log( + "[Codex Setup] storeApiKey available:", + !!setupApi?.storeApiKey + ); if (setupApi?.storeApiKey) { console.log("[Codex Setup] Calling storeApiKey for openai..."); @@ -920,7 +1055,9 @@ function CodexSetupStep({ console.log("[Codex Setup] storeApiKey result:", result); if (result.success) { - console.log("[Codex Setup] API key stored successfully, updating state..."); + console.log( + "[Codex Setup] API key stored successfully, updating state..." + ); setApiKeys({ ...apiKeys, openai: apiKey }); setCodexAuthStatus({ authenticated: true, @@ -933,7 +1070,9 @@ function CodexSetupStep({ 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"); + console.log( + "[Codex Setup] Web mode - storing API key in app state only" + ); setApiKeys({ ...apiKeys, openai: apiKey }); setCodexAuthStatus({ authenticated: true, @@ -957,13 +1096,14 @@ function CodexSetupStep({ }; 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)"; + if (codexAuthStatus?.method === "cli_verified") + return "CLI Login (ChatGPT)"; return "Authenticated"; }; @@ -1031,7 +1171,10 @@ function CodexSetupStep({ )}
) : ( - + )}
@@ -1114,9 +1257,7 @@ function CodexSetupStep({ Authentication - - Codex requires an OpenAI API key - + Codex requires an OpenAI API key {codexCliStatus?.installed && ( @@ -1236,7 +1377,8 @@ function CodexSetupStep({ Codex is ready to use!

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

@@ -1381,22 +1523,34 @@ function CompleteStep({ onFinish }: { onFinish: () => void }) { // Main Setup View export function SetupView() { - const { currentStep, setCurrentStep, completeSetup, setSkipClaudeSetup, setSkipCodexSetup } = - useSetupStore(); + const { + currentStep, + setCurrentStep, + completeSetup, + setSkipClaudeSetup, + setSkipCodexSetup, + } = useSetupStore(); const { setCurrentView } = useAppStore(); const steps = ["welcome", "claude", "codex", "complete"] as const; - type StepName = typeof steps[number]; + 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 === "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); + console.log( + "[Setup Flow] handleNext called from:", + from, + "currentStep:", + currentStep + ); switch (from) { case "welcome": console.log("[Setup Flow] Moving to claude_detect step"); @@ -1445,10 +1599,7 @@ export function SetupView() { }; return ( -
+
{/* Header */}
@@ -1467,7 +1618,10 @@ export function SetupView() {
- +
diff --git a/app/src/lib/project-init.ts b/app/src/lib/project-init.ts index 977a10c4..e2484427 100644 --- a/app/src/lib/project-init.ts +++ b/app/src/lib/project-init.ts @@ -57,7 +57,7 @@ export async function initializeProject( const exists = await api.exists(fullPath); if (!exists) { - await api.writeFile(fullPath, defaultContent); + await api.writeFile(fullPath, defaultContent as string); createdFiles.push(relativePath); } else { existingFiles.push(relativePath); diff --git a/app/src/types/electron.d.ts b/app/src/types/electron.d.ts index 46ce7e2e..213b793d 100644 --- a/app/src/types/electron.d.ts +++ b/app/src/types/electron.d.ts @@ -272,7 +272,10 @@ export interface SpecRegenerationAPI { } export interface AutoModeAPI { - start: (projectPath: string, maxConcurrency?: number) => Promise<{ + start: ( + projectPath: string, + maxConcurrency?: number + ) => Promise<{ success: boolean; error?: string; }>; @@ -299,25 +302,38 @@ export interface AutoModeAPI { error?: string; }>; - runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => Promise<{ + runFeature: ( + projectPath: string, + featureId: string, + useWorktrees?: boolean + ) => Promise<{ success: boolean; passes?: boolean; error?: string; }>; - verifyFeature: (projectPath: string, featureId: string) => Promise<{ + verifyFeature: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; passes?: boolean; error?: string; }>; - resumeFeature: (projectPath: string, featureId: string) => Promise<{ + resumeFeature: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; passes?: boolean; error?: string; }>; - contextExists: (projectPath: string, featureId: string) => Promise<{ + contextExists: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; exists?: boolean; error?: string; @@ -329,13 +345,21 @@ export interface AutoModeAPI { error?: string; }>; - followUpFeature: (projectPath: string, featureId: string, prompt: string, imagePaths?: string[]) => Promise<{ + followUpFeature: ( + projectPath: string, + featureId: string, + prompt: string, + imagePaths?: string[] + ) => Promise<{ success: boolean; passes?: boolean; error?: string; }>; - commitFeature: (projectPath: string, featureId: string) => Promise<{ + commitFeature: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; error?: string; }>; @@ -345,6 +369,9 @@ export interface AutoModeAPI { export interface ElectronAPI { ping: () => Promise; + openExternalLink: ( + url: string + ) => Promise<{ success: boolean; error?: string }>; // Dialog APIs openDirectory: () => Promise<{ @@ -362,7 +389,10 @@ export interface ElectronAPI { content?: string; error?: string; }>; - writeFile: (filePath: string, content: string) => Promise<{ + writeFile: ( + filePath: string, + content: string + ) => Promise<{ success: boolean; error?: string; }>; @@ -525,25 +555,35 @@ export interface FileDiffResult { export interface WorktreeAPI { // Revert feature changes by removing the worktree - revertFeature: (projectPath: string, featureId: string) => Promise<{ + revertFeature: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; removedPath?: string; error?: string; }>; // Merge feature worktree changes back to main branch - mergeFeature: (projectPath: string, featureId: string, options?: { - squash?: boolean; - commitMessage?: string; - squashMessage?: string; - }) => Promise<{ + mergeFeature: ( + projectPath: string, + featureId: string, + options?: { + squash?: boolean; + commitMessage?: string; + squashMessage?: string; + } + ) => Promise<{ success: boolean; mergedBranch?: string; error?: string; }>; // Get worktree info for a feature - getInfo: (projectPath: string, featureId: string) => Promise<{ + getInfo: ( + projectPath: string, + featureId: string + ) => Promise<{ success: boolean; worktreePath?: string; branchName?: string; @@ -552,7 +592,10 @@ export interface WorktreeAPI { }>; // Get worktree status (changed files, commits) - getStatus: (projectPath: string, featureId: string) => Promise; + getStatus: ( + projectPath: string, + featureId: string + ) => Promise; // List all feature worktrees list: (projectPath: string) => Promise<{ @@ -562,10 +605,17 @@ export interface WorktreeAPI { }>; // Get file diffs for a feature worktree - getDiffs: (projectPath: string, featureId: string) => Promise; + getDiffs: ( + projectPath: string, + featureId: string + ) => Promise; // Get diff for a specific file in a worktree - getFileDiff: (projectPath: string, featureId: string, filePath: string) => Promise; + getFileDiff: ( + projectPath: string, + featureId: string, + filePath: string + ) => Promise; } export interface GitAPI { @@ -573,7 +623,10 @@ export interface GitAPI { getDiffs: (projectPath: string) => Promise; // Get diff for a specific file in the main project - getFileDiff: (projectPath: string, filePath: string) => Promise; + getFileDiff: ( + projectPath: string, + filePath: string + ) => Promise; } // Model definition type