Merge branch 'fs/ui' into removing-electron-features-build-api

Resolved conflict in http-api-client.ts by adopting the server-side
file browser dialog approach from fs/ui branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alec Koifman
2025-12-12 17:38:05 -05:00
16 changed files with 810 additions and 192 deletions

View File

@@ -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
<project_specification>
<project_name>[App Name]</project_name>
<overview>
[Description of what the app does and its purpose]
</overview>
<technology_stack>
<frontend>[frameworks, libraries]</frontend>
<backend>[frameworks, APIs]</backend>
<database>[if applicable]</database>
</technology_stack>
<core_capabilities>
[List all the major capabilities]
</core_capabilities>
<implementation_roadmap>
<phase_1>[Foundation features]</phase_1>
<phase_2>[Core features]</phase_2>
<phase_3>[Polish features]</phase_3>
</implementation_roadmap>
</project_specification>
\`\`\`
**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\`.`;
}
/**

View File

@@ -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() {
</main>
);
}
export default function Home() {
return (
<FileBrowserProvider>
<HomeContent />
</FileBrowserProvider>
);
}

View File

@@ -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<string>("");
const [parentPath, setParentPath] = useState<string | null>(null);
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
const [drives, setDrives] = useState<string[]>([]);
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderOpen className="w-5 h-5 text-brand-500" />
{title}
</DialogTitle>
<DialogDescription className="text-muted-foreground">
{description}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 min-h-[400px]">
{/* Drives selector (Windows only) */}
{drives.length > 0 && (
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-2">
<HardDrive className="w-3 h-3" />
<span>Drives:</span>
</div>
{drives.map((drive) => (
<Button
key={drive}
variant={currentPath.startsWith(drive) ? "default" : "outline"}
size="sm"
onClick={() => handleSelectDrive(drive)}
className="h-7 px-3 text-xs"
disabled={loading}
>
{drive.replace("\\", "")}
</Button>
))}
</div>
)}
{/* Current path breadcrumb */}
<div className="flex items-center gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<Button
variant="ghost"
size="sm"
onClick={handleGoHome}
className="h-7 px-2"
disabled={loading}
>
<Home className="w-4 h-4" />
</Button>
{parentPath && (
<Button
variant="ghost"
size="sm"
onClick={handleGoToParent}
className="h-7 px-2"
disabled={loading}
>
<ArrowLeft className="w-4 h-4" />
</Button>
)}
<div className="flex-1 font-mono text-sm truncate text-muted-foreground">
{currentPath || "Loading..."}
</div>
</div>
{/* Directory list */}
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-lg">
{loading && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-muted-foreground">Loading directories...</div>
</div>
)}
{error && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-destructive">{error}</div>
</div>
)}
{!loading && !error && directories.length === 0 && (
<div className="flex items-center justify-center h-full p-8">
<div className="text-sm text-muted-foreground">No subdirectories found</div>
</div>
)}
{!loading && !error && directories.length > 0 && (
<div className="divide-y divide-sidebar-border">
{directories.map((dir) => (
<button
key={dir.path}
onClick={() => handleSelectDirectory(dir)}
className="w-full flex items-center gap-3 p-3 hover:bg-sidebar-accent/10 transition-colors text-left group"
>
<Folder className="w-5 h-5 text-brand-500 shrink-0" />
<span className="flex-1 truncate text-sm">{dir.name}</span>
<ChevronRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</button>
))}
</div>
)}
</div>
<div className="text-xs text-muted-foreground">
Click on a folder to navigate. Select the current folder or navigate to a subfolder.
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSelect} disabled={!currentPath || loading}>
<FolderOpen className="w-4 h-4 mr-2" />
Select Current Folder
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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`}
</span>
</div>

View File

@@ -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)

View File

@@ -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];

View File

@@ -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: <CheckCircle2 className="w-4 h-4" />,
}
);
} else if (isIntermediateCompletion) {
// Intermediate completion - keep state active for feature generation
setIsCreating(true);
setIsRegenerating(true);

View File

@@ -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<string | null>;
}
const FileBrowserContext = createContext<FileBrowserContextValue | null>(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<string | null> => {
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 (
<FileBrowserContext.Provider value={{ openFileBrowser }}>
{children}
<FileBrowserDialog
open={isOpen}
onOpenChange={handleOpenChange}
onSelect={handleSelect}
/>
</FileBrowserContext.Provider>
);
}
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<string | null>) | null = null;
export function setGlobalFileBrowser(fn: () => Promise<string | null>) {
globalFileBrowserFn = fn;
}
export function getGlobalFileBrowser() {
return globalFileBrowserFn;
}

View File

@@ -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<DialogResult> {
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<DialogResult> {
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

View File

@@ -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

View File

@@ -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

View File

@@ -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<string[]> => {
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 {

View File

@@ -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);

View File

@@ -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 ==========");
}