diff --git a/apps/app/electron/services/spec-regeneration-service.js b/apps/app/electron/services/spec-regeneration-service.js index 3376d334..1a3c5d52 100644 --- a/apps/app/electron/services/spec-regeneration-service.js +++ b/apps/app/electron/services/spec-regeneration-service.js @@ -605,7 +605,7 @@ You should: 4. Based on the user's project overview, create a comprehensive app specification 5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application 6. Use the XML template format provided -7. Write the specification to .automaker/app_spec.txt +7. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** When analyzing, look at: - package.json, cargo.toml, requirements.txt or similar config files for tech stack @@ -615,11 +615,17 @@ When analyzing, look at: - API structures and patterns You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your primary target) +- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) -You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec. +You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. -**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase.`; +**IMPORTANT:** Focus ONLY on creating the app_spec.txt file. Do NOT create any feature files or use any feature management tools during this phase. + +**CRITICAL FILE NAMING RULES:** +- The spec file MUST be named exactly \`app_spec.txt\` +- Do NOT create project-spec.md, spec.md, or any other filename +- Do NOT use markdown (.md) extension - use .txt +- The full path must be: \`.automaker/app_spec.txt\``; } /** @@ -656,7 +662,11 @@ ${APP_SPEC_XML_TEMPLATE} - **development_workflow**: Note any testing or development patterns - **implementation_roadmap**: Break down the features into phases - be VERY detailed here, listing every feature that needs to be built -4. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` +4. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` + - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name + - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename + - Do NOT output the spec in your response - write it to the file + - Use the Write tool with path \`.automaker/app_spec.txt\` **Guidelines:** - Be comprehensive! Include ALL features needed for a complete application @@ -665,8 +675,9 @@ ${APP_SPEC_XML_TEMPLATE} - The implementation_roadmap should reflect logical phases for building out the app - list EVERY feature individually - Consider user flows, error states, and edge cases when defining features - Each phase should have multiple specific, actionable features +- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** -Begin by exploring the project structure.`; +Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; } /** @@ -865,7 +876,7 @@ You should: 3. Understand the current architecture and patterns used 4. Based on the user's project definition, create a comprehensive app specification that includes ALL features needed to realize their vision 5. Be liberal and comprehensive when defining features - include everything needed for a complete, polished application -6. Write the specification to .automaker/app_spec.txt +6. **MANDATORY: Write the spec to EXACTLY \`.automaker/app_spec.txt\` - this exact filename, no alternatives** When analyzing, look at: - package.json, cargo.toml, or similar config files for tech stack @@ -878,9 +889,15 @@ When analyzing, look at: Your task is ONLY to update the app_spec.txt file - feature files will be managed separately. You CAN and SHOULD modify: -- .automaker/app_spec.txt (this is your primary target) +- .automaker/app_spec.txt (this is your ONLY target file - use EXACTLY this filename) -You have access to file reading, writing, and search tools. Use them to understand the codebase and write the new spec.`; +You have access to file reading, writing, and search tools. Use them to understand the codebase and WRITE the new spec to .automaker/app_spec.txt. + +**CRITICAL FILE NAMING RULES:** +- The spec file MUST be named exactly \`app_spec.txt\` +- Do NOT create project-spec.md, spec.md, or any other filename +- Do NOT use markdown (.md) extension - use .txt +- The full path must be: \`.automaker/app_spec.txt\``; } /** @@ -909,37 +926,40 @@ ${projectDefinition} - Think about user experience, error handling, edge cases, etc. - Architecture Notes: Any important architectural decisions or patterns -3. **IMPORTANT**: Write the complete specification to the file \`.automaker/app_spec.txt\` +3. **MANDATORY FILE WRITE**: You MUST write the spec to EXACTLY this file path: \`.automaker/app_spec.txt\` + - The filename MUST be exactly \`app_spec.txt\` - do NOT use any other name + - Do NOT create \`project-spec.md\`, \`spec.md\`, or any other filename + - Do NOT output the spec in your response - write it to the file + - Use the Write tool with path \`.automaker/app_spec.txt\` -**Format Guidelines for the Spec:** +**Format Guidelines for the Spec (use XML format in app_spec.txt):** -Use this general structure: +Use this XML structure inside app_spec.txt: -\`\`\` -# [App Name] - Application Specification - -## Product Overview -[Description of what the app does and its purpose] - -## Tech Stack -- Frontend: [frameworks, libraries] -- Backend: [frameworks, APIs] -- Database: [if applicable] -- Other: [other relevant tech] - -## Features - -### [Category 1] -- **[Feature Name]**: [Detailed description of the feature] -- **[Feature Name]**: [Detailed description] -... - -### [Category 2] -- **[Feature Name]**: [Detailed description] -... - -## Architecture Notes -[Any important architectural notes, patterns, or conventions] +\`\`\`xml + + [App Name] + + + [Description of what the app does and its purpose] + + + + [frameworks, libraries] + [frameworks, APIs] + [if applicable] + + + + [List all the major capabilities] + + + + [Foundation features] + [Core features] + [Polish features] + + \`\`\` **Remember:** @@ -947,9 +967,9 @@ Use this general structure: - Consider user flows, error states, loading states, etc. - Include authentication, authorization if relevant - Think about what would make this a polished, production-ready app -- The more detailed and complete the spec, the better +- **CRITICAL: Write to EXACTLY \`.automaker/app_spec.txt\` - not project-spec.md or any other name!** -Begin by exploring the project structure.`; +Begin by exploring the project structure, then generate and WRITE the spec to \`.automaker/app_spec.txt\`.`; } /** diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index e2260c86..14e200be 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -15,8 +15,9 @@ import { RunningAgentsView } from "@/components/views/running-agents-view"; import { useAppStore } from "@/store/app-store"; import { useSetupStore } from "@/store/setup-store"; import { getElectronAPI, isElectron } from "@/lib/electron"; +import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context"; -export default function Home() { +function HomeContent() { const { currentView, setCurrentView, @@ -27,6 +28,7 @@ export default function Home() { const { isFirstRun, setupComplete } = useSetupStore(); const [isMounted, setIsMounted] = useState(false); const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); + const { openFileBrowser } = useFileBrowser(); // Hidden streamer panel - opens with "\" key const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => { @@ -79,6 +81,11 @@ export default function Home() { setIsMounted(true); }, []); + // Initialize global file browser for HttpApiClient + useEffect(() => { + setGlobalFileBrowser(openFileBrowser); + }, [openFileBrowser]); + // Check if this is first run and redirect to setup if needed useEffect(() => { console.log("[Setup Flow] Checking setup state:", { @@ -236,3 +243,11 @@ export default function Home() { ); } + +export default function Home() { + return ( + + + + ); +} diff --git a/apps/app/src/components/dialogs/file-browser-dialog.tsx b/apps/app/src/components/dialogs/file-browser-dialog.tsx new file mode 100644 index 00000000..29c183f1 --- /dev/null +++ b/apps/app/src/components/dialogs/file-browser-dialog.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { FolderOpen, Folder, ChevronRight, Home, ArrowLeft, HardDrive } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +interface DirectoryEntry { + name: string; + path: string; +} + +interface BrowseResult { + success: boolean; + currentPath: string; + parentPath: string | null; + directories: DirectoryEntry[]; + drives?: string[]; + error?: string; +} + +interface FileBrowserDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSelect: (path: string) => void; + title?: string; + description?: string; +} + +export function FileBrowserDialog({ + open, + onOpenChange, + onSelect, + title = "Select Project Directory", + description = "Navigate to your project folder", +}: FileBrowserDialogProps) { + const [currentPath, setCurrentPath] = useState(""); + const [parentPath, setParentPath] = useState(null); + const [directories, setDirectories] = useState([]); + const [drives, setDrives] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const browseDirectory = async (dirPath?: string) => { + setLoading(true); + setError(""); + + try { + // Get server URL from environment or default + const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3008"; + + const response = await fetch(`${serverUrl}/api/fs/browse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ dirPath }), + }); + + const result: BrowseResult = await response.json(); + + if (result.success) { + setCurrentPath(result.currentPath); + setParentPath(result.parentPath); + setDirectories(result.directories); + setDrives(result.drives || []); + } else { + setError(result.error || "Failed to browse directory"); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load directories"); + } finally { + setLoading(false); + } + }; + + // Load home directory on mount + useEffect(() => { + if (open && !currentPath) { + browseDirectory(); + } + }, [open]); + + const handleSelectDirectory = (dir: DirectoryEntry) => { + browseDirectory(dir.path); + }; + + const handleGoToParent = () => { + if (parentPath) { + browseDirectory(parentPath); + } + }; + + const handleGoHome = () => { + browseDirectory(); + }; + + const handleSelectDrive = (drivePath: string) => { + browseDirectory(drivePath); + }; + + const handleSelect = () => { + if (currentPath) { + onSelect(currentPath); + onOpenChange(false); + } + }; + + return ( + + + + + + {title} + + + {description} + + + +
+ {/* Drives selector (Windows only) */} + {drives.length > 0 && ( +
+
+ + Drives: +
+ {drives.map((drive) => ( + + ))} +
+ )} + + {/* Current path breadcrumb */} +
+ + {parentPath && ( + + )} +
+ {currentPath || "Loading..."} +
+
+ + {/* Directory list */} +
+ {loading && ( +
+
Loading directories...
+
+ )} + + {error && ( +
+
{error}
+
+ )} + + {!loading && !error && directories.length === 0 && ( +
+
No subdirectories found
+
+ )} + + {!loading && !error && directories.length > 0 && ( +
+ {directories.map((dir) => ( + + ))} +
+ )} +
+ +
+ Click on a folder to navigate. Select the current folder or navigate to a subfolder. +
+
+ + + + + +
+
+ ); +} diff --git a/apps/app/src/components/views/analysis-view.tsx b/apps/app/src/components/views/analysis-view.tsx index 530ef2f9..a7a9e85f 100644 --- a/apps/app/src/components/views/analysis-view.tsx +++ b/apps/app/src/components/views/analysis-view.tsx @@ -399,7 +399,7 @@ ${Object.entries(projectAnalysis.filesByExtension) `; // Write the spec file - const specPath = `${currentProject.path}/app_spec.txt`; + const specPath = `${currentProject.path}/.automaker/app_spec.txt`; const writeResult = await api.writeFile(specPath, specContent); if (writeResult.success) { diff --git a/apps/app/src/components/views/kanban-card.tsx b/apps/app/src/components/views/kanban-card.tsx index 7888de3e..41a35729 100644 --- a/apps/app/src/components/views/kanban-card.tsx +++ b/apps/app/src/components/views/kanban-card.tsx @@ -207,10 +207,12 @@ export const KanbanCard = memo(function KanbanCard({ // - Backlog items can always be dragged // - skipTests items can be dragged even when in_progress or verified (unless currently running) // - waiting_approval items can always be dragged (to allow manual verification via drag) - // - Non-skipTests (TDD) items in progress or verified cannot be dragged + // - verified items can always be dragged (to allow moving back to waiting_approval or backlog) + // - Non-skipTests (TDD) items in progress cannot be dragged (they are running) const isDraggable = feature.status === "backlog" || feature.status === "waiting_approval" || + feature.status === "verified" || (feature.skipTests && !isCurrentAutoTask); const { attributes, diff --git a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx index 94a49338..0c36b2ef 100644 --- a/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx +++ b/apps/app/src/components/views/settings-view/api-keys/authentication-status-display.tsx @@ -61,13 +61,15 @@ export function AuthenticationStatusDisplay({ {claudeAuthStatus.method === "oauth_token_env" ? "Using CLAUDE_CODE_OAUTH_TOKEN" : claudeAuthStatus.method === "oauth_token" - ? "Using stored OAuth token (claude login)" + ? "Using stored OAuth token (subscription)" : claudeAuthStatus.method === "api_key_env" ? "Using ANTHROPIC_API_KEY" : claudeAuthStatus.method === "api_key" ? "Using stored API key" : claudeAuthStatus.method === "credentials_file" ? "Using credentials file" + : claudeAuthStatus.method === "cli_authenticated" + ? "Using Claude CLI authentication" : `Using ${claudeAuthStatus.method || "detected"} authentication`} diff --git a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts index 3f4422c4..600a5f67 100644 --- a/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts +++ b/apps/app/src/components/views/settings-view/hooks/use-cli-status.ts @@ -74,8 +74,8 @@ export function useCliStatus() { apiKeyValid?: boolean; }; // Map server method names to client method types - // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, none - const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "none"] as const; + // Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none + const validMethods = ["oauth_token_env", "oauth_token", "api_key", "api_key_env", "credentials_file", "cli_authenticated", "none"] as const; type AuthMethod = typeof validMethods[number]; const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) ? (auth.method as AuthMethod) diff --git a/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts b/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts index b7a31685..1aa0d094 100644 --- a/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts +++ b/apps/app/src/components/views/setup-view/hooks/use-cli-status.ts @@ -40,6 +40,8 @@ export function useCliStatus({ "oauth_token", "api_key", "api_key_env", + "credentials_file", + "cli_authenticated", "none", ] as const; type AuthMethod = (typeof validMethods)[number]; diff --git a/apps/app/src/components/views/spec-view.tsx b/apps/app/src/components/views/spec-view.tsx index 87173e9b..3fbf4ffe 100644 --- a/apps/app/src/components/views/spec-view.tsx +++ b/apps/app/src/components/views/spec-view.tsx @@ -14,7 +14,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus } from "lucide-react"; +import { Save, RefreshCw, FileText, Sparkles, Loader2, FilePlus2, AlertCircle, ListPlus, CheckCircle2 } from "lucide-react"; +import { toast } from "sonner"; import { Checkbox } from "@/components/ui/checkbox"; import { XmlSyntaxEditor } from "@/components/ui/xml-syntax-editor"; import type { SpecRegenerationEvent } from "@/types/electron"; @@ -311,14 +312,22 @@ export function SpecView() { // The backend sends explicit signals for completion: // 1. "All tasks completed" in the message // 2. [Phase: complete] marker in logs + // 3. "Spec regeneration complete!" for regeneration + // 4. "Initial spec creation complete!" for creation without features const isFinalCompletionMessage = event.message?.includes("All tasks completed") || event.message === "All tasks completed!" || - event.message === "All tasks completed"; + event.message === "All tasks completed" || + event.message === "Spec regeneration complete!" || + event.message === "Initial spec creation complete!"; const hasCompletePhase = logsRef.current.includes("[Phase: complete]"); + // Intermediate completion means features are being generated after spec creation + const isIntermediateCompletion = event.message?.includes("Features are being generated") || + event.message?.includes("features are being generated"); + // Rely solely on explicit backend signals - const shouldComplete = isFinalCompletionMessage || hasCompletePhase; + const shouldComplete = (isFinalCompletionMessage || hasCompletePhase) && !isIntermediateCompletion; if (shouldComplete) { // Fully complete - clear all states immediately @@ -337,9 +346,29 @@ export function SpecView() { setProjectOverview(""); setErrorMessage(""); stateRestoredRef.current = false; - // Reload the spec to show the new content - loadSpec(); - } else { + + // Reload the spec with delay to ensure file is written to disk + setTimeout(() => { + loadSpec(); + }, SPEC_FILE_WRITE_DELAY); + + // Show success toast notification + const isRegeneration = event.message?.includes("regeneration"); + const isFeatureGeneration = event.message?.includes("Feature generation"); + toast.success( + isFeatureGeneration + ? "Feature Generation Complete" + : isRegeneration + ? "Spec Regeneration Complete" + : "Spec Creation Complete", + { + description: isFeatureGeneration + ? "Features have been created from the app specification." + : "Your app specification has been saved.", + icon: , + } + ); + } else if (isIntermediateCompletion) { // Intermediate completion - keep state active for feature generation setIsCreating(true); setIsRegenerating(true); diff --git a/apps/app/src/contexts/file-browser-context.tsx b/apps/app/src/contexts/file-browser-context.tsx new file mode 100644 index 00000000..f54fb27f --- /dev/null +++ b/apps/app/src/contexts/file-browser-context.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, type ReactNode } from "react"; +import { FileBrowserDialog } from "@/components/dialogs/file-browser-dialog"; + +interface FileBrowserContextValue { + openFileBrowser: () => Promise; +} + +const FileBrowserContext = createContext(null); + +export function FileBrowserProvider({ children }: { children: ReactNode }) { + const [isOpen, setIsOpen] = useState(false); + const [resolver, setResolver] = useState<((value: string | null) => void) | null>(null); + + const openFileBrowser = useCallback((): Promise => { + return new Promise((resolve) => { + setIsOpen(true); + setResolver(() => resolve); + }); + }, []); + + const handleSelect = useCallback((path: string) => { + if (resolver) { + resolver(path); + setResolver(null); + } + setIsOpen(false); + }, [resolver]); + + const handleOpenChange = useCallback((open: boolean) => { + if (!open && resolver) { + resolver(null); + setResolver(null); + } + setIsOpen(open); + }, [resolver]); + + return ( + + {children} + + + ); +} + +export function useFileBrowser() { + const context = useContext(FileBrowserContext); + if (!context) { + throw new Error("useFileBrowser must be used within FileBrowserProvider"); + } + return context; +} + +// Global reference for non-React code (like HttpApiClient) +let globalFileBrowserFn: (() => Promise) | null = null; + +export function setGlobalFileBrowser(fn: () => Promise) { + globalFileBrowserFn = fn; +} + +export function getGlobalFileBrowser() { + return globalFileBrowserFn; +} diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts index d11d6044..76313ee1 100644 --- a/apps/app/src/lib/http-api-client.ts +++ b/apps/app/src/lib/http-api-client.ts @@ -31,7 +31,7 @@ import type { ModelDefinition, ProviderStatus, } from "@/types/electron"; -import { openDirectoryPicker, openFilePicker, type DirectoryPickerResult } from "./file-picker"; +import { getGlobalFileBrowser } from "@/contexts/file-browser-context"; // Server URL - configurable via environment variable @@ -202,96 +202,62 @@ export class HttpApiClient implements ElectronAPI { return { success: true }; } - // File picker - uses web-based file picker (works on Windows) + // File picker - uses server-side file browser dialog async openDirectory(): Promise { - try { - console.log("[HttpApiClient] Opening directory picker..."); - const directoryInfo = await openDirectoryPicker(); - console.log("[HttpApiClient] Directory info:", directoryInfo); - - if (!directoryInfo) { - console.log("[HttpApiClient] No directory selected (user canceled)"); - return { canceled: true, filePaths: [] }; - } + const fileBrowser = getGlobalFileBrowser(); - // Try to resolve directory path using server endpoint - // First, try if we have an absolute path (from file.path property) - if (directoryInfo.directoryName && (directoryInfo.directoryName.includes("\\") || directoryInfo.directoryName.includes("/") || directoryInfo.directoryName.startsWith("/"))) { - // Looks like an absolute path, try validating it directly - console.log("[HttpApiClient] Attempting direct path validation:", directoryInfo.directoryName); - const directResult = await this.post<{ - success: boolean; - path?: string; - error?: string; - }>("/api/fs/validate-path", { filePath: directoryInfo.directoryName }); - - if (directResult.success && directResult.path) { - console.log("[HttpApiClient] Direct path validation succeeded:", directResult.path); - return { canceled: false, filePaths: [directResult.path] }; - } - } - - // If direct validation failed or we only have a directory name, - // use the resolve endpoint with directory structure - console.log("[HttpApiClient] Resolving directory using structure info..."); - const result = await this.post<{ - success: boolean; - path?: string; - error?: string; - }>("/api/fs/resolve-directory", { - directoryName: directoryInfo.directoryName, - sampleFiles: directoryInfo.sampleFiles, - fileCount: directoryInfo.fileCount, - }); - - console.log("[HttpApiClient] Directory resolution result:", result); - - if (result.success && result.path) { - console.log("[HttpApiClient] Directory resolved successfully:", result.path); - return { canceled: false, filePaths: [result.path] }; - } - - // If resolution failed, show error - console.warn("[HttpApiClient] Directory resolution failed:", result.error); - const errorMsg = result.error || "Could not locate directory. Please ensure the directory exists and try selecting it again."; - alert(errorMsg); - return { canceled: true, filePaths: [] }; - } catch (error) { - console.error("[HttpApiClient] Failed to open directory picker:", error); - alert("Failed to open directory picker. Please try again."); + if (!fileBrowser) { + console.error("File browser not initialized"); return { canceled: true, filePaths: [] }; } + + const path = await fileBrowser(); + + if (!path) { + return { canceled: true, filePaths: [] }; + } + + // Validate with server + const result = await this.post<{ + success: boolean; + path?: string; + error?: string; + }>("/api/fs/validate-path", { filePath: path }); + + if (result.success && result.path) { + return { canceled: false, filePaths: [result.path] }; + } + + console.error("Invalid directory:", result.error); + return { canceled: true, filePaths: [] }; } async openFile(options?: object): Promise { - try { - const selectedPath = await openFilePicker(options); - if (!selectedPath) { - return { canceled: true, filePaths: [] }; - } + const fileBrowser = getGlobalFileBrowser(); - // Handle both single file and multiple files - const filePaths = Array.isArray(selectedPath) ? selectedPath : [selectedPath]; - - // Validate files exist with server - // For multiple files, check the first one as a validation step - const firstPath = filePaths[0]; - const result = await this.post<{ success: boolean; exists: boolean }>( - "/api/fs/exists", - { filePath: firstPath } - ); - - if (result.success && result.exists) { - return { canceled: false, filePaths }; - } - - alert("File does not exist or cannot be accessed."); - return { canceled: true, filePaths: [] }; - } catch (error) { - console.error("[HttpApiClient] Failed to open file picker:", error); - alert("Failed to open file picker. Please try again."); + if (!fileBrowser) { + console.error("File browser not initialized"); return { canceled: true, filePaths: [] }; } + + // For now, use the same directory browser (could be enhanced for file selection) + const path = await fileBrowser(); + + if (!path) { + return { canceled: true, filePaths: [] }; + } + + const result = await this.post<{ success: boolean; exists: boolean }>( + "/api/fs/exists", + { filePath: path } + ); + + if (result.success && result.exists) { + return { canceled: false, filePaths: [path] }; + } + + console.error("File not found"); + return { canceled: true, filePaths: [] }; } // File system operations diff --git a/apps/app/src/store/setup-store.ts b/apps/app/src/store/setup-store.ts index 0265e64b..15714d28 100644 --- a/apps/app/src/store/setup-store.ts +++ b/apps/app/src/store/setup-store.ts @@ -17,6 +17,7 @@ export type ClaudeAuthMethod = | "api_key_env" // ANTHROPIC_API_KEY environment variable | "api_key" // Manually stored API key | "credentials_file" // Generic credentials file detection + | "cli_authenticated" // Claude CLI is installed and has active sessions/activity | "none"; // Claude Auth Status diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2c4821b2..90238848 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -39,30 +39,30 @@ const PORT = parseInt(process.env.PORT || "3008", 10); const DATA_DIR = process.env.DATA_DIR || "./data"; // Check for required environment variables +// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use) const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN; -if (!hasAnthropicKey) { +if (!hasAnthropicKey && !hasOAuthToken) { console.warn(` ╔═══════════════════════════════════════════════════════════════════════╗ -║ ⚠️ WARNING: ANTHROPIC_API_KEY not set ║ +║ ⚠️ WARNING: No Claude authentication configured ║ ║ ║ -║ The Claude Agent SDK requires ANTHROPIC_API_KEY to function. ║ -║ ${ - hasOAuthToken - ? " You have CLAUDE_CODE_OAUTH_TOKEN set - this is for CLI auth only." - : "" - } +║ The Claude Agent SDK requires authentication to function. ║ ║ ║ -║ Set your API key: ║ +║ Option 1 - Subscription (OAuth Token): ║ +║ export CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" ║ +║ ║ +║ Option 2 - Pay-per-use (API Key): ║ ║ export ANTHROPIC_API_KEY="sk-ant-..." ║ ║ ║ -║ Or add to apps/server/.env: ║ -║ ANTHROPIC_API_KEY=sk-ant-... ║ +║ Or use the setup wizard in Settings to configure authentication. ║ ╚═══════════════════════════════════════════════════════════════════════╝ `); +} else if (hasOAuthToken) { + console.log("[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)"); } else { - console.log("[Server] ✓ ANTHROPIC_API_KEY detected"); + console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)"); } // Initialize security diff --git a/apps/server/src/routes/fs.ts b/apps/server/src/routes/fs.ts index 3aff1d70..ac492f03 100644 --- a/apps/server/src/routes/fs.ts +++ b/apps/server/src/routes/fs.ts @@ -7,6 +7,7 @@ import { Router, type Request, type Response } from "express"; import fs from "fs/promises"; import os from "os"; import path from "path"; +import os from "os"; import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js"; import type { EventEmitter } from "../lib/events.js"; @@ -371,6 +372,82 @@ export function createFsRoutes(_events: EventEmitter): Router { } }); + // Browse directories - for file browser UI + router.post("/browse", async (req: Request, res: Response) => { + try { + const { dirPath } = req.body as { dirPath?: string }; + + // Default to home directory if no path provided + const targetPath = dirPath ? path.resolve(dirPath) : os.homedir(); + + // Detect available drives on Windows + const detectDrives = async (): Promise => { + if (os.platform() !== "win32") { + return []; + } + + const drives: string[] = []; + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + for (const letter of letters) { + const drivePath = `${letter}:\\`; + try { + await fs.access(drivePath); + drives.push(drivePath); + } catch { + // Drive doesn't exist, skip it + } + } + + return drives; + }; + + try { + const stats = await fs.stat(targetPath); + + if (!stats.isDirectory()) { + res.status(400).json({ success: false, error: "Path is not a directory" }); + return; + } + + // Read directory contents + const entries = await fs.readdir(targetPath, { withFileTypes: true }); + + // Filter for directories only and add parent directory option + const directories = entries + .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")) + .map((entry) => ({ + name: entry.name, + path: path.join(targetPath, entry.name), + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + // Get parent directory + const parentPath = path.dirname(targetPath); + const hasParent = parentPath !== targetPath; + + // Get available drives + const drives = await detectDrives(); + + res.json({ + success: true, + currentPath: targetPath, + parentPath: hasParent ? parentPath : null, + directories, + drives, + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : "Failed to read directory", + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + res.status(500).json({ success: false, error: message }); + } + }); + // Serve image files router.get("/image", async (req: Request, res: Response) => { try { diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts index 48cbec89..a1b5b38b 100644 --- a/apps/server/src/routes/setup.ts +++ b/apps/server/src/routes/setup.ts @@ -107,12 +107,14 @@ export function createSetupRoutes(): Router { } // Check authentication - detect all possible auth methods + // Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth + // apiKeys.anthropic stores direct API keys for pay-per-use let auth = { authenticated: false, method: "none" as string, hasCredentialsFile: false, hasToken: false, - hasStoredOAuthToken: false, + hasStoredOAuthToken: !!apiKeys.anthropic_oauth_token, hasStoredApiKey: !!apiKeys.anthropic, hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY, hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN, @@ -199,9 +201,17 @@ export function createSetupRoutes(): Router { auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var } - // In-memory stored API key (from settings UI) + // In-memory stored OAuth token (from setup wizard - subscription auth) + if (!auth.authenticated && apiKeys.anthropic_oauth_token) { + auth.authenticated = true; + auth.oauthTokenValid = true; + auth.method = "oauth_token"; // Stored OAuth token from setup wizard + } + + // In-memory stored API key (from settings UI - pay-per-use) if (!auth.authenticated && apiKeys.anthropic) { auth.authenticated = true; + auth.apiKeyValid = true; auth.method = "api_key"; // Manually stored API key } @@ -393,9 +403,19 @@ export function createSetupRoutes(): Router { apiKeys[provider] = apiKey; // Also set as environment variable and persist to .env - if (provider === "anthropic" || provider === "anthropic_oauth_token") { + // IMPORTANT: OAuth tokens and API keys must be stored separately + // - OAuth tokens (subscription auth) -> CLAUDE_CODE_OAUTH_TOKEN + // - API keys (pay-per-use) -> ANTHROPIC_API_KEY + if (provider === "anthropic_oauth_token") { + // OAuth token from claude setup-token (subscription-based auth) + process.env.CLAUDE_CODE_OAUTH_TOKEN = apiKey; + await persistApiKeyToEnv("CLAUDE_CODE_OAUTH_TOKEN", apiKey); + console.log("[Setup] Stored OAuth token as CLAUDE_CODE_OAUTH_TOKEN"); + } else if (provider === "anthropic") { + // Direct API key (pay-per-use) process.env.ANTHROPIC_API_KEY = apiKey; await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey); + console.log("[Setup] Stored API key as ANTHROPIC_API_KEY"); } else if (provider === "openai") { process.env.OPENAI_API_KEY = apiKey; await persistApiKeyToEnv("OPENAI_API_KEY", apiKey); diff --git a/apps/server/src/routes/spec-regeneration.ts b/apps/server/src/routes/spec-regeneration.ts index b0e52aa1..c9373675 100644 --- a/apps/server/src/routes/spec-regeneration.ts +++ b/apps/server/src/routes/spec-regeneration.ts @@ -11,11 +11,28 @@ import type { EventEmitter } from "../lib/events.js"; let isRunning = false; let currentAbortController: AbortController | null = null; +// Helper to log authentication status +function logAuthStatus(context: string): void { + const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN; + const hasApiKey = !!process.env.ANTHROPIC_API_KEY; + + console.log(`[SpecRegeneration] ${context} - Auth Status:`); + console.log(`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${hasOAuthToken ? 'SET (' + process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...)' : 'NOT SET'}`); + console.log(`[SpecRegeneration] ANTHROPIC_API_KEY: ${hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'}`); + + if (!hasOAuthToken && !hasApiKey) { + console.error(`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`); + } +} + export function createSpecRegenerationRoutes(events: EventEmitter): Router { const router = Router(); // Create project spec from overview router.post("/create", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /create endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath, projectOverview, generateFeatures } = req.body as { projectPath: string; @@ -23,7 +40,13 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { generateFeatures?: boolean; }; + console.log(`[SpecRegeneration] Parsed params:`); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectOverview length: ${projectOverview?.length || 0} chars`); + console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`); + if (!projectPath || !projectOverview) { + console.error("[SpecRegeneration] Missing required parameters"); res.status(400).json({ success: false, error: "projectPath and projectOverview required", @@ -32,12 +55,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Spec generation already running" }); return; } + logAuthStatus("Before starting generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background generation task..."); // Start generation in background generateSpec( @@ -48,19 +75,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { generateFeatures ) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "spec_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -68,13 +103,21 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { // Generate from project definition router.post("/generate", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /generate endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath, projectDefinition } = req.body as { projectPath: string; projectDefinition: string; }; + console.log(`[SpecRegeneration] Parsed params:`); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectDefinition length: ${projectDefinition?.length || 0} chars`); + if (!projectPath || !projectDefinition) { + console.error("[SpecRegeneration] Missing required parameters"); res.status(400).json({ success: false, error: "projectPath and projectDefinition required", @@ -83,12 +126,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Spec generation already running" }); return; } + logAuthStatus("Before starting generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background generation task..."); generateSpec( projectPath, @@ -98,19 +145,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { false ) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "spec_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -118,37 +173,55 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router { // Generate features from existing spec router.post("/generate-features", async (req: Request, res: Response) => { + console.log("[SpecRegeneration] ========== /generate-features endpoint called =========="); + console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2)); + try { const { projectPath } = req.body as { projectPath: string }; + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + if (!projectPath) { + console.error("[SpecRegeneration] Missing projectPath parameter"); res.status(400).json({ success: false, error: "projectPath required" }); return; } if (isRunning) { + console.warn("[SpecRegeneration] Generation already running, rejecting request"); res.json({ success: false, error: "Generation already running" }); return; } + logAuthStatus("Before starting feature generation"); + isRunning = true; currentAbortController = new AbortController(); + console.log("[SpecRegeneration] Starting background feature generation task..."); generateFeaturesFromSpec(projectPath, events, currentAbortController) .catch((error) => { - console.error("[SpecRegeneration] Error:", error); + console.error("[SpecRegeneration] ❌ Feature generation failed with error:"); + console.error("[SpecRegeneration] Error name:", error?.name); + console.error("[SpecRegeneration] Error message:", error?.message); + console.error("[SpecRegeneration] Error stack:", error?.stack); + console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2)); events.emit("spec-regeneration:event", { type: "features_error", - error: error.message, + error: error.message || String(error), }); }) .finally(() => { + console.log("[SpecRegeneration] Feature generation task finished (success or error)"); isRunning = false; currentAbortController = null; }); + console.log("[SpecRegeneration] Returning success response (generation running in background)"); res.json({ success: true }); } catch (error) { + console.error("[SpecRegeneration] ❌ Route handler exception:"); + console.error("[SpecRegeneration] Error:", error); const message = error instanceof Error ? error.message : "Unknown error"; res.status(500).json({ success: false, error: message }); } @@ -188,6 +261,11 @@ async function generateSpec( abortController: AbortController, generateFeatures?: boolean ) { + console.log("[SpecRegeneration] ========== generateSpec() started =========="); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + console.log(`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`); + console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`); + const prompt = `You are helping to define a software project specification. Project Overview: @@ -214,6 +292,8 @@ Also generate a list of features to implement. For each feature provide: Format your response as markdown. Be specific and actionable.`; + console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); + events.emit("spec-regeneration:event", { type: "spec_progress", content: "Starting spec generation...\n", @@ -228,38 +308,78 @@ Format your response as markdown. Be specific and actionable.`; abortController, }; - const stream = query({ prompt, options }); - let responseText = ""; + console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2)); + console.log("[SpecRegeneration] Calling Claude Agent SDK query()..."); + + // Log auth status right before the SDK call + logAuthStatus("Right before SDK query()"); - for await (const msg of stream) { - if (msg.type === "assistant" && msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText = block.text; - events.emit("spec-regeneration:event", { - type: "spec_progress", - content: block.text, - }); - } else if (block.type === "tool_use") { - events.emit("spec-regeneration:event", { - type: "spec_tool", - tool: block.name, - input: block.input, - }); - } - } - } else if (msg.type === "result" && msg.subtype === "success") { - responseText = msg.result || responseText; - } + let stream; + try { + stream = query({ prompt, options }); + console.log("[SpecRegeneration] query() returned stream successfully"); + } catch (queryError) { + console.error("[SpecRegeneration] ❌ query() threw an exception:"); + console.error("[SpecRegeneration] Error:", queryError); + throw queryError; } + let responseText = ""; + let messageCount = 0; + + console.log("[SpecRegeneration] Starting to iterate over stream..."); + + try { + for await (const msg of stream) { + messageCount++; + console.log(`[SpecRegeneration] Stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)); + + if (msg.type === "assistant" && msg.message.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText = block.text; + console.log(`[SpecRegeneration] Text block received (${block.text.length} chars)`); + events.emit("spec-regeneration:event", { + type: "spec_progress", + content: block.text, + }); + } else if (block.type === "tool_use") { + console.log(`[SpecRegeneration] Tool use: ${block.name}`); + events.emit("spec-regeneration:event", { + type: "spec_tool", + tool: block.name, + input: block.input, + }); + } + } + } else if (msg.type === "result" && (msg as any).subtype === "success") { + console.log("[SpecRegeneration] Received success result"); + responseText = (msg as any).result || responseText; + } else if (msg.type === "error") { + console.error("[SpecRegeneration] ❌ Received error message from stream:"); + console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2)); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ❌ Error while iterating stream:"); + console.error("[SpecRegeneration] Stream error:", streamError); + throw streamError; + } + + console.log(`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`); + console.log(`[SpecRegeneration] Response text length: ${responseText.length} chars`); + // Save spec const specDir = path.join(projectPath, ".automaker"); const specPath = path.join(specDir, "app_spec.txt"); + console.log(`[SpecRegeneration] Saving spec to: ${specPath}`); + await fs.mkdir(specDir, { recursive: true }); await fs.writeFile(specPath, responseText); + console.log("[SpecRegeneration] Spec saved successfully"); + events.emit("spec-regeneration:event", { type: "spec_complete", specPath, @@ -268,8 +388,11 @@ Format your response as markdown. Be specific and actionable.`; // If generate features was requested, parse and create them if (generateFeatures) { + console.log("[SpecRegeneration] Starting feature generation..."); await parseAndCreateFeatures(projectPath, responseText, events); } + + console.log("[SpecRegeneration] ========== generateSpec() completed =========="); } async function generateFeaturesFromSpec( @@ -277,13 +400,20 @@ async function generateFeaturesFromSpec( events: EventEmitter, abortController: AbortController ) { + console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() started =========="); + console.log(`[SpecRegeneration] projectPath: ${projectPath}`); + // Read existing spec const specPath = path.join(projectPath, ".automaker", "app_spec.txt"); let spec: string; + console.log(`[SpecRegeneration] Reading spec from: ${specPath}`); + try { spec = await fs.readFile(specPath, "utf-8"); - } catch { + console.log(`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`); + } catch (readError) { + console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError); events.emit("spec-regeneration:event", { type: "features_error", error: "No project spec found. Generate spec first.", @@ -320,6 +450,8 @@ Format as JSON: Generate 5-15 features that build on each other logically.`; + console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`); + events.emit("spec-regeneration:event", { type: "features_progress", content: "Analyzing spec and generating features...\n", @@ -334,26 +466,62 @@ Generate 5-15 features that build on each other logically.`; abortController, }; - const stream = query({ prompt, options }); - let responseText = ""; + console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2)); + console.log("[SpecRegeneration] Calling Claude Agent SDK query() for features..."); + + logAuthStatus("Right before SDK query() for features"); - for await (const msg of stream) { - if (msg.type === "assistant" && msg.message.content) { - for (const block of msg.message.content) { - if (block.type === "text") { - responseText = block.text; - events.emit("spec-regeneration:event", { - type: "features_progress", - content: block.text, - }); - } - } - } else if (msg.type === "result" && msg.subtype === "success") { - responseText = msg.result || responseText; - } + let stream; + try { + stream = query({ prompt, options }); + console.log("[SpecRegeneration] query() returned stream successfully"); + } catch (queryError) { + console.error("[SpecRegeneration] ❌ query() threw an exception:"); + console.error("[SpecRegeneration] Error:", queryError); + throw queryError; } + let responseText = ""; + let messageCount = 0; + + console.log("[SpecRegeneration] Starting to iterate over feature stream..."); + + try { + for await (const msg of stream) { + messageCount++; + console.log(`[SpecRegeneration] Feature stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)); + + if (msg.type === "assistant" && msg.message.content) { + for (const block of msg.message.content) { + if (block.type === "text") { + responseText = block.text; + console.log(`[SpecRegeneration] Feature text block received (${block.text.length} chars)`); + events.emit("spec-regeneration:event", { + type: "features_progress", + content: block.text, + }); + } + } + } else if (msg.type === "result" && (msg as any).subtype === "success") { + console.log("[SpecRegeneration] Received success result for features"); + responseText = (msg as any).result || responseText; + } else if (msg.type === "error") { + console.error("[SpecRegeneration] ❌ Received error message from feature stream:"); + console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2)); + } + } + } catch (streamError) { + console.error("[SpecRegeneration] ❌ Error while iterating feature stream:"); + console.error("[SpecRegeneration] Stream error:", streamError); + throw streamError; + } + + console.log(`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`); + console.log(`[SpecRegeneration] Feature response length: ${responseText.length} chars`); + await parseAndCreateFeatures(projectPath, responseText, events); + + console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() completed =========="); } async function parseAndCreateFeatures( @@ -361,20 +529,31 @@ async function parseAndCreateFeatures( content: string, events: EventEmitter ) { + console.log("[SpecRegeneration] ========== parseAndCreateFeatures() started =========="); + console.log(`[SpecRegeneration] Content length: ${content.length} chars`); + try { // Extract JSON from response + console.log("[SpecRegeneration] Extracting JSON from response..."); const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/); if (!jsonMatch) { + console.error("[SpecRegeneration] ❌ No valid JSON found in response"); + console.error("[SpecRegeneration] Content preview:", content.substring(0, 500)); throw new Error("No valid JSON found in response"); } + console.log(`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`); + const parsed = JSON.parse(jsonMatch[0]); + console.log(`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`); + const featuresDir = path.join(projectPath, ".automaker", "features"); await fs.mkdir(featuresDir, { recursive: true }); const createdFeatures: Array<{ id: string; title: string }> = []; for (const feature of parsed.features) { + console.log(`[SpecRegeneration] Creating feature: ${feature.id}`); const featureDir = path.join(featuresDir, feature.id); await fs.mkdir(featureDir, { recursive: true }); @@ -398,15 +577,21 @@ async function parseAndCreateFeatures( createdFeatures.push({ id: feature.id, title: feature.title }); } + console.log(`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`); + events.emit("spec-regeneration:event", { type: "features_complete", features: createdFeatures, count: createdFeatures.length, }); } catch (error) { + console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:"); + console.error("[SpecRegeneration] Error:", error); events.emit("spec-regeneration:event", { type: "features_error", error: (error as Error).message, }); } + + console.log("[SpecRegeneration] ========== parseAndCreateFeatures() completed =========="); }