mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge branch 'main' into new-project-from-template
This commit is contained in:
@@ -1,202 +0,0 @@
|
||||
<project_specification>
|
||||
<project_name>Automaker - Autonomous AI Development Studio</project_name>
|
||||
|
||||
<overview>
|
||||
Automaker is a sophisticated desktop application that empowers developers to build software autonomously through AI-powered agents. Built with Electron and Next.js, it provides an intelligent GUI for project management, feature tracking via Kanban boards, and autonomous code generation. The application leverages multiple AI models (Claude, GPT) and supports complex workflows including git worktree isolation, testing automation, and multi-model agent execution. It acts as a complete development orchestrator, managing the entire lifecycle from specification to verified implementation.
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<frontend>
|
||||
<framework>Next.js 16.0.7 (App Router)</framework>
|
||||
<ui_library>shadcn/ui with Radix UI primitives</ui_library>
|
||||
<styling>Tailwind CSS 4.0</styling>
|
||||
<state_management>Zustand with persistence</state_management>
|
||||
<drag_drop>@dnd-kit for Kanban board</drag_drop>
|
||||
<icons>Lucide React</icons>
|
||||
<query_client>TanStack Query for server state</query_client>
|
||||
</frontend>
|
||||
<desktop_shell>
|
||||
<framework>Electron 39.2.6</framework>
|
||||
<language>TypeScript 5.x</language>
|
||||
<inter_process_communication>Electron IPC with security sandboxing</inter_process_communication>
|
||||
<file_system>Node.js fs/promises with path validation</file_system>
|
||||
</desktop_shell>
|
||||
<ai_engine>
|
||||
<primary_model>Claude 3.5 (Opus, Sonnet, Haiku) via Anthropic Claude Agent SDK</primary_model>
|
||||
<secondary_model>GPT-5.1 Codex family via OpenAI CLI</secondary_model>
|
||||
<orchestration>Custom Agent Service with streaming responses</orchestration>
|
||||
<model_registry>Dynamic model provider system with CLI detection</model_registry>
|
||||
</ai_engine>
|
||||
<testing>
|
||||
<framework>Playwright for E2E testing</framework>
|
||||
<unit>Jest/Vitest compatible</unit>
|
||||
<integration>Agent-driven test execution and verification</integration>
|
||||
</testing>
|
||||
<version_control>
|
||||
<system>Git with worktree isolation support</system>
|
||||
<branching>Feature branch management</branching>
|
||||
<workflow>Automated commit and merge capabilities</workflow>
|
||||
</version_control>
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<project_management>
|
||||
- Open and manage multiple local projects
|
||||
- Project-specific themes and configurations
|
||||
- Session management with project context
|
||||
- Recently used project cycling (Q/E shortcuts)
|
||||
- Project search and type-ahead selection
|
||||
- Trash and restore functionality for projects
|
||||
</project_management>
|
||||
|
||||
<intelligent_analysis>
|
||||
- Auto-generation and updating of app_spec.txt
|
||||
- Feature extraction from existing codebases
|
||||
- Technology stack detection and documentation
|
||||
- Project structure analysis with file tree visualization - "Project Ingestion": Analyzes existing codebases to understand structure
|
||||
- Auto-generation of `.automaker/app_spec.txt` based on codebase analysis
|
||||
- Auto-generation of features in `.automaker/features/{id}/feature.json`:
|
||||
- Scans code for implemented features
|
||||
- Creates test cases for existing features
|
||||
- Marks existing features as "passes": true automatically
|
||||
</intelligent_analysis>
|
||||
|
||||
<kanban_workflow>
|
||||
- Visual representation of features from `.automaker/features/` folder
|
||||
- Drag-and-drop interface to reprioritize tasks
|
||||
- direct editing of feature details (steps, description) from the card
|
||||
- Visual Kanban board with drag-and-drop functionality
|
||||
- Multiple status columns: Backlog, In Progress, Waiting Approval, Verified
|
||||
- Feature cards with detailed information display (3 detail levels)
|
||||
- Real-time status updates during agent execution
|
||||
- Search and filtering capabilities
|
||||
- Category management and autocomplete
|
||||
- Image attachment support for feature descriptions
|
||||
</kanban_workflow>
|
||||
|
||||
<autonomous_agent_engine>
|
||||
- Multi-model agent system with profile-based execution
|
||||
- Streaming agent output with real-time logs
|
||||
- Git worktree isolation for safe feature development
|
||||
- Automatic testing and verification workflows
|
||||
- Context-aware prompt generation
|
||||
- Agent memory and learning capabilities
|
||||
- Concurrent feature processing with configurable limits
|
||||
- Follow-up and resume capabilities
|
||||
</autonomous_agent_engine>
|
||||
|
||||
<advanced_workflows>
|
||||
- Git worktree management for isolated development
|
||||
- Feature-specific branching and merging
|
||||
- Automated commit generation with file tracking
|
||||
- Test-driven development support
|
||||
- Code review and approval workflows
|
||||
- Revert and rollback capabilities
|
||||
</advanced_workflows>
|
||||
|
||||
<user_interface>
|
||||
- Dark/Light theme support with 12 custom themes
|
||||
- Per-project theme configurations
|
||||
- Comprehensive keyboard shortcut system
|
||||
- Sidebar navigation with project switching
|
||||
- Multi-view architecture (Board, Spec, Agent, Context, Settings)
|
||||
- Setup wizard for first-time configuration
|
||||
- CLI integration status monitoring
|
||||
</user_interface>
|
||||
|
||||
<extensibility>
|
||||
- AI Profile system for model/thinking level presets
|
||||
- Keyboard shortcut customization
|
||||
- Model provider plugin architecture
|
||||
- Context file management for agent guidance
|
||||
- Feature suggestion generation
|
||||
- Spec regeneration workflows
|
||||
</extensibility>
|
||||
</core_capabilities>
|
||||
|
||||
<ui_layout>
|
||||
<window_structure>
|
||||
- Sidebar: Project List, Settings, Logs, Plugins
|
||||
- Main Content:
|
||||
- **Spec View**: Split editor for `.automaker/app_spec.txt`
|
||||
- **Board View**: Kanban board for `.automaker/features/` folder
|
||||
- **Code View**: Read-only Monaco editor to see what the agent is writing
|
||||
- **Agent View**: Chat-like interface showing agent thought process and tool usage. Also used for the "New Project Interview".
|
||||
</window_structure>
|
||||
<theme>
|
||||
- Dark/Light mode support (system sync)
|
||||
- "Hacker" aesthetic option (terminal-like)
|
||||
- Professional/Clean default
|
||||
</theme>
|
||||
</ui_layout>
|
||||
|
||||
<development_workflow>
|
||||
<local_testing>
|
||||
- "Browser Mode": Run the Next.js frontend in a standard browser with mocked Electron IPC for rapid UI iteration.
|
||||
- "Electron Mode": Full desktop app testing.
|
||||
- Hot Reloading for both Main and Renderer processes.
|
||||
</local_testing>
|
||||
</development_workflow>
|
||||
|
||||
<implemented_features>
|
||||
- Complete Kanban board with drag-and-drop functionality
|
||||
- Multi-model AI agent execution (Claude + GPT/Codex)
|
||||
- Git worktree isolation for features
|
||||
- Real-time agent output streaming and logging
|
||||
- Project management with session persistence
|
||||
- Theme system with 12 themes + per-project themes
|
||||
- Comprehensive settings panel with all configurations
|
||||
- Feature image attachment and context system
|
||||
- Agent profiles with model/thinking level presets
|
||||
- Keyboard shortcut system with customization
|
||||
- CLI integration detection (Claude Code + Codex CLI)
|
||||
- Auto mode for autonomous feature processing
|
||||
- Feature suggestions generation
|
||||
- Spec regeneration and project analysis
|
||||
- Context file management
|
||||
- Chat history and session management
|
||||
- File diff viewing and git integration
|
||||
- Search and filtering across all features
|
||||
- Category management and autocomplete
|
||||
- Test automation and verification workflows
|
||||
</implemented_features>
|
||||
|
||||
<implementation_roadmap>
|
||||
<phase_1_foundation>
|
||||
- Enhanced error handling and recovery mechanisms
|
||||
- Performance optimization for large projects
|
||||
- Improved memory management for long-running sessions
|
||||
- Advanced logging and debugging capabilities
|
||||
</phase_1_foundation>
|
||||
|
||||
<phase_2_core_logic>
|
||||
- Plugin system for custom model providers
|
||||
- Advanced workflow customization engine
|
||||
- Team collaboration features
|
||||
- Cloud synchronization capabilities
|
||||
- Advanced project templates and scaffolding
|
||||
</phase_2_core_logic>
|
||||
|
||||
<phase_3_kanban_and_interaction>
|
||||
- Build Kanban board with drag-and-drop
|
||||
- Connect Kanban state to `.automaker/features/` filesystem
|
||||
- Implement "Run Feature" capability
|
||||
- Integrate standard prompts library
|
||||
</phase_3_kanban_and_interaction>
|
||||
|
||||
<phase_3_polish>
|
||||
- Enhanced accessibility features
|
||||
- Advanced theme customization
|
||||
- Performance monitoring and analytics
|
||||
- Documentation generation automation
|
||||
- Integration with external development tools
|
||||
- Advanced security auditing and sandboxing
|
||||
</phase_3_polish>
|
||||
|
||||
<phase_4_polish>
|
||||
- Advanced terminal integration
|
||||
- Settings & Extensibility
|
||||
- UI refinement
|
||||
</phase_4_polish>
|
||||
</implementation_roadmap>
|
||||
</project_specification>
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
"Agent Runner",
|
||||
"Core",
|
||||
"Kanban",
|
||||
"Other",
|
||||
"Settings",
|
||||
"Uncategorized",
|
||||
"ka"
|
||||
]
|
||||
@@ -1,474 +0,0 @@
|
||||
# Clean Code Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
This document serves as a comprehensive guide for writing clean, maintainable, and extensible code. It outlines principles and practices that ensure code quality, reusability, and long-term maintainability. When writing or reviewing code, follow these guidelines to create software that is easy to understand, modify, and extend. This file is used by LLMs to understand and enforce coding standards throughout the codebase.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. DRY (Don't Repeat Yourself)
|
||||
|
||||
**Principle**: Every piece of knowledge should have a single, unambiguous representation within a system.
|
||||
|
||||
**Practices**:
|
||||
|
||||
- Extract repeated logic into reusable functions, classes, or modules
|
||||
- Use constants for repeated values
|
||||
- Create shared utilities for common operations
|
||||
- Avoid copy-pasting code blocks
|
||||
- When you find yourself writing similar code more than twice, refactor it
|
||||
|
||||
**Example - Bad**:
|
||||
|
||||
```typescript
|
||||
// Repeated validation logic
|
||||
if (email.includes("@") && email.length > 5) {
|
||||
// ...
|
||||
}
|
||||
if (email.includes("@") && email.length > 5) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Good**:
|
||||
|
||||
```typescript
|
||||
function isValidEmail(email: string): boolean {
|
||||
return email.includes("@") && email.length > 5;
|
||||
}
|
||||
|
||||
if (isValidEmail(email)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Code Reusability
|
||||
|
||||
**Principle**: Write code that can be used in multiple contexts without modification or with minimal adaptation.
|
||||
|
||||
**Practices**:
|
||||
|
||||
- Create generic, parameterized functions instead of specific ones
|
||||
- Use composition over inheritance where appropriate
|
||||
- Design functions to be pure (no side effects) when possible
|
||||
- Create utility libraries for common operations
|
||||
- Use dependency injection to make components reusable
|
||||
- Design APIs that are flexible and configurable
|
||||
|
||||
**Example - Bad**:
|
||||
|
||||
```typescript
|
||||
function calculateUserTotal(userId: string) {
|
||||
const user = getUser(userId);
|
||||
return user.items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Good**:
|
||||
|
||||
```typescript
|
||||
function calculateTotal<T extends { price: number }>(items: T[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
|
||||
function calculateUserTotal(userId: string) {
|
||||
const user = getUser(userId);
|
||||
return calculateTotal(user.items);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Abstract Functions and Abstractions
|
||||
|
||||
**Principle**: Create abstractions that hide implementation details and provide clear, simple interfaces.
|
||||
|
||||
**Practices**:
|
||||
|
||||
- Use interfaces and abstract classes to define contracts
|
||||
- Create abstraction layers between different concerns
|
||||
- Hide complex implementation behind simple function signatures
|
||||
- Use dependency inversion - depend on abstractions, not concretions
|
||||
- Create factory functions/classes for object creation
|
||||
- Use strategy pattern for interchangeable algorithms
|
||||
|
||||
**Example - Bad**:
|
||||
|
||||
```typescript
|
||||
function processPayment(amount: number, cardNumber: string, cvv: string) {
|
||||
// Direct implementation tied to specific payment processor
|
||||
fetch("https://stripe.com/api/charge", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ amount, cardNumber, cvv }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Good**:
|
||||
|
||||
```typescript
|
||||
interface PaymentProcessor {
|
||||
processPayment(
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
): Promise<PaymentResult>;
|
||||
}
|
||||
|
||||
class StripeProcessor implements PaymentProcessor {
|
||||
async processPayment(
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
): Promise<PaymentResult> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
function processPayment(
|
||||
processor: PaymentProcessor,
|
||||
amount: number,
|
||||
details: PaymentDetails
|
||||
) {
|
||||
return processor.processPayment(amount, details);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Extensibility
|
||||
|
||||
**Principle**: Design code that can be easily extended with new features without modifying existing code.
|
||||
|
||||
**Practices**:
|
||||
|
||||
- Follow the Open/Closed Principle: open for extension, closed for modification
|
||||
- Use plugin architectures and hooks for extensibility
|
||||
- Design with future requirements in mind (but don't over-engineer)
|
||||
- Use configuration over hardcoding
|
||||
- Create extension points through interfaces and callbacks
|
||||
- Use composition and dependency injection
|
||||
- Design APIs that can accommodate new parameters/options
|
||||
|
||||
**Example - Bad**:
|
||||
|
||||
```typescript
|
||||
function sendNotification(user: User, type: string) {
|
||||
if (type === "email") {
|
||||
sendEmail(user.email);
|
||||
} else if (type === "sms") {
|
||||
sendSMS(user.phone);
|
||||
}
|
||||
// Adding new notification types requires modifying this function
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Good**:
|
||||
|
||||
```typescript
|
||||
interface NotificationChannel {
|
||||
send(user: User): Promise<void>;
|
||||
}
|
||||
|
||||
class EmailChannel implements NotificationChannel {
|
||||
async send(user: User): Promise<void> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
class SMSChannel implements NotificationChannel {
|
||||
async send(user: User): Promise<void> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationService {
|
||||
constructor(private channels: NotificationChannel[]) {}
|
||||
|
||||
async send(user: User): Promise<void> {
|
||||
await Promise.all(this.channels.map((channel) => channel.send(user)));
|
||||
}
|
||||
}
|
||||
// New notification types can be added without modifying existing code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Avoid Magic Numbers and Strings
|
||||
|
||||
**Principle**: Use named constants instead of hardcoded values to improve readability and maintainability.
|
||||
|
||||
**Practices**:
|
||||
|
||||
- Extract all magic numbers into named constants
|
||||
- Use enums for related constants
|
||||
- Create configuration objects for settings
|
||||
- Use constants for API endpoints, timeouts, limits, etc.
|
||||
- Document why specific values are used
|
||||
|
||||
**Example - Bad**:
|
||||
|
||||
```typescript
|
||||
if (user.age >= 18) {
|
||||
// What does 18 mean?
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// What does 3000 mean?
|
||||
}, 3000);
|
||||
|
||||
if (status === "active") {
|
||||
// What are the valid statuses?
|
||||
}
|
||||
```
|
||||
|
||||
**Example - Good**:
|
||||
|
||||
```typescript
|
||||
const MINIMUM_AGE_FOR_ADULTS = 18;
|
||||
const SESSION_TIMEOUT_MS = 3000;
|
||||
|
||||
enum UserStatus {
|
||||
ACTIVE = "active",
|
||||
INACTIVE = "inactive",
|
||||
SUSPENDED = "suspended",
|
||||
}
|
||||
|
||||
if (user.age >= MINIMUM_AGE_FOR_ADULTS) {
|
||||
// Clear intent
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// Clear intent
|
||||
}, SESSION_TIMEOUT_MS);
|
||||
|
||||
if (status === UserStatus.ACTIVE) {
|
||||
// Type-safe and clear
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Best Practices
|
||||
|
||||
### 6. Single Responsibility Principle
|
||||
|
||||
Each function, class, or module should have one reason to change.
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Bad: Multiple responsibilities
|
||||
class User {
|
||||
save() {
|
||||
/* database logic */
|
||||
}
|
||||
sendEmail() {
|
||||
/* email logic */
|
||||
}
|
||||
validate() {
|
||||
/* validation logic */
|
||||
}
|
||||
}
|
||||
|
||||
// Good: Single responsibility
|
||||
class User {
|
||||
validate() {
|
||||
/* validation only */
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
save(user: User) {
|
||||
/* database logic */
|
||||
}
|
||||
}
|
||||
|
||||
class EmailService {
|
||||
sendToUser(user: User) {
|
||||
/* email logic */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Meaningful Names
|
||||
|
||||
- Use descriptive names that reveal intent
|
||||
- Avoid abbreviations unless they're widely understood
|
||||
- Use verbs for functions, nouns for classes
|
||||
- Be consistent with naming conventions
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
const d = new Date();
|
||||
const u = getUser();
|
||||
function calc(x, y) {}
|
||||
|
||||
// Good
|
||||
const currentDate = new Date();
|
||||
const currentUser = getUser();
|
||||
function calculateTotal(price: number, quantity: number): number {}
|
||||
```
|
||||
|
||||
### 8. Small Functions
|
||||
|
||||
- Functions should do one thing and do it well
|
||||
- Keep functions short (ideally under 20 lines)
|
||||
- Extract complex logic into separate functions
|
||||
- Use descriptive function names instead of comments
|
||||
|
||||
### 9. Error Handling
|
||||
|
||||
- Handle errors explicitly
|
||||
- Use appropriate error types
|
||||
- Provide meaningful error messages
|
||||
- Don't swallow errors silently
|
||||
- Use try-catch appropriately
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
function divide(a: number, b: number) {
|
||||
return a / b; // Can throw division by zero
|
||||
}
|
||||
|
||||
// Good
|
||||
function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero is not allowed");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Comments and Documentation
|
||||
|
||||
- Write self-documenting code (code should explain itself)
|
||||
- Use comments to explain "why", not "what"
|
||||
- Document complex algorithms or business logic
|
||||
- Keep comments up-to-date with code changes
|
||||
- Use JSDoc/TSDoc for public APIs
|
||||
|
||||
### 11. Type Safety
|
||||
|
||||
- Use TypeScript types/interfaces effectively
|
||||
- Avoid `any` type unless absolutely necessary
|
||||
- Use union types and discriminated unions
|
||||
- Leverage type inference where appropriate
|
||||
- Create custom types for domain concepts
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
function processUser(data: any) {
|
||||
return data.name;
|
||||
}
|
||||
|
||||
// Good
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
function processUser(user: User): string {
|
||||
return user.name;
|
||||
}
|
||||
```
|
||||
|
||||
### 12. Testing Considerations
|
||||
|
||||
- Write testable code (pure functions, dependency injection)
|
||||
- Keep functions small and focused
|
||||
- Avoid hidden dependencies
|
||||
- Use mocks and stubs appropriately
|
||||
- Design for testability from the start
|
||||
|
||||
### 13. Performance vs. Readability
|
||||
|
||||
- Prefer readability over premature optimization
|
||||
- Profile before optimizing
|
||||
- Use clear algorithms first, optimize if needed
|
||||
- Document performance-critical sections
|
||||
- Balance between clean code and performance requirements
|
||||
|
||||
### 14. Code Organization
|
||||
|
||||
- Group related functionality together
|
||||
- Use modules/packages to organize code
|
||||
- Follow consistent file and folder structures
|
||||
- Separate concerns (UI, business logic, data access)
|
||||
- Use barrel exports (index files) appropriately
|
||||
|
||||
### 15. Configuration Management
|
||||
|
||||
- Externalize configuration values
|
||||
- Use environment variables for environment-specific settings
|
||||
- Create configuration objects/interfaces
|
||||
- Validate configuration at startup
|
||||
- Provide sensible defaults
|
||||
|
||||
**Example**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
const apiUrl = "https://api.example.com";
|
||||
const timeout = 5000;
|
||||
|
||||
// Good
|
||||
interface Config {
|
||||
apiUrl: string;
|
||||
timeout: number;
|
||||
maxRetries: number;
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
apiUrl: process.env.API_URL || "https://api.example.com",
|
||||
timeout: parseInt(process.env.TIMEOUT || "5000"),
|
||||
maxRetries: parseInt(process.env.MAX_RETRIES || "3"),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
When reviewing code, check for:
|
||||
|
||||
- [ ] No code duplication (DRY principle)
|
||||
- [ ] Meaningful variable and function names
|
||||
- [ ] No magic numbers or strings
|
||||
- [ ] Functions are small and focused
|
||||
- [ ] Proper error handling
|
||||
- [ ] Type safety maintained
|
||||
- [ ] Code is testable
|
||||
- [ ] Documentation where needed
|
||||
- [ ] Consistent code style
|
||||
- [ ] Proper abstraction levels
|
||||
- [ ] Extensibility considered
|
||||
- [ ] Single responsibility principle followed
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Clean code is:
|
||||
|
||||
- **Readable**: Easy to understand at a glance
|
||||
- **Maintainable**: Easy to modify and update
|
||||
- **Testable**: Easy to write tests for
|
||||
- **Extensible**: Easy to add new features
|
||||
- **Reusable**: Can be used in multiple contexts
|
||||
- **Well-documented**: Clear intent and purpose
|
||||
- **Type-safe**: Leverages type system effectively
|
||||
- **DRY**: No unnecessary repetition
|
||||
- **Abstracted**: Proper separation of concerns
|
||||
- **Configurable**: Uses constants and configuration over hardcoding
|
||||
|
||||
Remember: Code is read far more often than it is written. Write code for your future self and your teammates.
|
||||
@@ -1,70 +0,0 @@
|
||||
You are a very strong reasoner and planner. Use these critical instructions to structure your plans, thoughts, and responses.
|
||||
|
||||
Before taking any action (either tool calls or responses to the user), you must proactively, methodically, and independently plan and reason about:
|
||||
|
||||
1. Logical dependencies and constraints:
|
||||
|
||||
Analyze the intended action against the following factors. Resolve conflicts in order of importance:
|
||||
|
||||
1.1) Policy-based rules, mandatory prerequisites, and constraints.
|
||||
1.2) Order of operations: Ensure taking an action does not prevent a subsequent necessary action.
|
||||
1.2.1) The user may request actions in a random order, but you may need to reorder operations to maximize successful completion of the task.
|
||||
1.3) Other prerequisites (information and/or actions needed).
|
||||
1.4) Explicit user constraints or preferences.
|
||||
|
||||
2. Risk assessment:
|
||||
|
||||
What are the consequences of taking the action? Will the new state cause any future issues?
|
||||
|
||||
2.1) For exploratory tasks (like searches), missing optional parameters is a LOW risk.
|
||||
Prefer calling the tool with the available information over asking the user, unless your Rule 1 (Logical Dependencies) reasoning determines that optional information is required for a later step in your plan.
|
||||
|
||||
3. Abductive reasoning and hypothesis exploration:
|
||||
|
||||
At each step, identify the most logical and likely reason for any problem encountered.
|
||||
|
||||
3.1) Look beyond immediate or obvious causes. The most likely reason may not be the simplest and may require deeper inference.
|
||||
3.2) Hypotheses may require additional research. Each hypothesis may take multiple steps to test.
|
||||
3.3) Prioritize hypotheses based on likelihood, but do not discard less likely ones prematurely. A low-probability event may still be the root cause.
|
||||
|
||||
4. Outcome evaluation and adaptability:
|
||||
|
||||
Does the previous observation require any changes to your plan?
|
||||
|
||||
4.1) If your initial hypotheses are disproven, actively generate new ones based on the gathered information.
|
||||
|
||||
5. Information availability:
|
||||
|
||||
Incorporate all applicable and alternative sources of information, including:
|
||||
|
||||
5.1) Using available tools and their capabilities
|
||||
5.2) All policies, rules, checklists, and constraints
|
||||
5.3) Previous observations and conversation history
|
||||
5.4) Information only available by asking the user
|
||||
|
||||
6. Precision and Grounding:
|
||||
|
||||
Ensure your reasoning is extremely precise and relevant to each exact ongoing situation.
|
||||
|
||||
6.1) Verify your claims by quoting the exact applicable information (including policies) when referring to them.
|
||||
|
||||
7. Completeness:
|
||||
|
||||
Ensure that all requirements, constraints, options, and preferences are exhaustively incorporated into your plan.
|
||||
|
||||
7.1) Resolve conflicts using the order of importance in #1.
|
||||
7.2) Avoid premature conclusions: There may be multiple relevant options for a given situation.
|
||||
7.2.1) To check for whether an option is relevant, reason about all information sources from #5.
|
||||
7.2.2) You may need to consult the user to even know whether something is applicable. Do not assume it is not applicable without checking.
|
||||
7.3) Review applicable sources of information from #5 to confirm which are relevant to the current state.
|
||||
|
||||
8. Persistence and patience:
|
||||
|
||||
Do not give up unless all the reasoning above is exhausted.
|
||||
|
||||
8.1) Don't be dissuaded by time taken or user frustration.
|
||||
8.2) This persistence must be intelligent: On transient errors (e.g. please try again), you must retry unless an explicit retry limit (e.g., max x tries) has been reached. If such a limit is hit, you must stop. On other errors, you must change your strategy or arguments, not repeat the same failed call.
|
||||
|
||||
9. Inhibit your response:
|
||||
|
||||
Only take an action after all the above reasoning is completed. Once you've taken an action, you cannot take it back.
|
||||
@@ -1,172 +0,0 @@
|
||||
# Agent Memory - Lessons Learned
|
||||
|
||||
This file documents issues encountered by previous agents and their solutions. Read this before starting work to avoid repeating mistakes.
|
||||
|
||||
## Testing Issues
|
||||
|
||||
### Issue: Mock project setup not navigating to board view
|
||||
|
||||
**Problem:** Setting `currentProject` in localStorage didn't automatically show the board view - app stayed on welcome view.
|
||||
**Fix:** The `currentView` state is not persisted in localStorage. Instead of trying to set it, have tests click on the recent project from the welcome view to trigger `setCurrentProject()` which handles the view transition properly.
|
||||
|
||||
```typescript
|
||||
// Don't do this:
|
||||
await setupMockProject(page); // Sets localStorage
|
||||
await page.goto("/");
|
||||
await waitForElement(page, "board-view"); // ❌ Fails - still on welcome view
|
||||
|
||||
// Do this instead:
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
await waitForElement(page, "welcome-view");
|
||||
const recentProject = page.locator(
|
||||
'[data-testid="recent-project-test-project-1"]'
|
||||
);
|
||||
await recentProject.click(); // ✅ Triggers proper view transition
|
||||
await waitForElement(page, "board-view");
|
||||
```
|
||||
|
||||
### Issue: View output button test IDs are conditional
|
||||
|
||||
**Problem:** Tests failed looking for `view-output-inprogress-${featureId}` when the actual button had `view-output-${featureId}`.
|
||||
**Fix:** The button test ID depends on whether the feature is actively running:
|
||||
|
||||
- `view-output-${featureId}` - shown when feature is in `runningAutoTasks` (actively running)
|
||||
- `view-output-inprogress-${featureId}` - shown when status is "in_progress" but NOT actively running
|
||||
|
||||
After dragging a feature to in_progress, wait for the `auto_mode_feature_start` event to fire before looking for the button:
|
||||
|
||||
```typescript
|
||||
// Wait for feature to start running
|
||||
const viewOutputButton = page
|
||||
.locator(
|
||||
`[data-testid="view-output-${featureId}"], [data-testid="view-output-inprogress-${featureId}"]`
|
||||
)
|
||||
.first();
|
||||
await expect(viewOutputButton).toBeVisible({ timeout: 8000 });
|
||||
```
|
||||
|
||||
### Issue: Elements not appearing due to async event timing
|
||||
|
||||
**Problem:** Tests checked for UI elements before async events (like `auto_mode_feature_start`) had fired and updated the UI.
|
||||
**Fix:** Add appropriate timeouts when waiting for elements that depend on async events. The mock auto mode takes ~2.4 seconds to complete, so allow sufficient time:
|
||||
|
||||
```typescript
|
||||
// Mock auto mode timing: ~2.4s + 1.5s delay = ~4s total
|
||||
await waitForAgentOutputModalHidden(page, { timeout: 10000 });
|
||||
```
|
||||
|
||||
### Issue: Slider interaction testing
|
||||
|
||||
**Problem:** Clicking on slider track didn't reliably set specific values.
|
||||
**Fix:** Use the slider's keyboard interaction or calculate the exact click position on the track. For max value, click on the rightmost edge of the track.
|
||||
|
||||
### Issue: Port binding blocked in sandbox mode
|
||||
|
||||
**Problem:** Playwright tests couldn't bind to port in sandbox mode.
|
||||
**Fix:** Tests don't need sandbox disabled - the issue was TEST_REUSE_SERVER environment variable. Make sure to start the dev server separately or let Playwright's webServer config handle it.
|
||||
|
||||
## Code Architecture
|
||||
|
||||
### Issue: Understanding store state persistence
|
||||
|
||||
**Problem:** Not all store state is persisted to localStorage.
|
||||
**Fix:** Check the `partialize` function in `app-store.ts` to see which state is persisted:
|
||||
|
||||
```typescript
|
||||
partialize: (state) => ({
|
||||
projects: state.projects,
|
||||
currentProject: state.currentProject,
|
||||
theme: state.theme,
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
apiKeys: state.apiKeys,
|
||||
chatSessions: state.chatSessions,
|
||||
chatHistoryOpen: state.chatHistoryOpen,
|
||||
maxConcurrency: state.maxConcurrency, // Added for concurrency feature
|
||||
});
|
||||
```
|
||||
|
||||
Note: `currentView` is NOT persisted - it's managed through actions.
|
||||
|
||||
### Issue: Auto mode task lifecycle
|
||||
|
||||
**Problem:** Confusion about when features are considered "running" vs "in_progress".
|
||||
**Fix:** Understand the task lifecycle:
|
||||
|
||||
1. Feature dragged to "in_progress" column → status becomes "in_progress"
|
||||
2. `auto_mode_feature_start` event fires → feature added to `runningAutoTasks`
|
||||
3. Agent works on feature → periodic events sent
|
||||
4. `auto_mode_feature_complete` event fires → feature removed from `runningAutoTasks`
|
||||
5. If `passes: true` → status becomes "verified", if `passes: false` → stays "in_progress"
|
||||
|
||||
### Issue: waiting_approval features not draggable when skipTests=true
|
||||
|
||||
**Problem:** Features in `waiting_approval` status couldn't be dragged to `verified` column, even though the code appeared to handle it.
|
||||
**Fix:** The order of condition checks in `handleDragEnd` matters. The `skipTests` check was catching `waiting_approval` features before the `waiting_approval` status check could handle them. Move the `waiting_approval` status check **before** the `skipTests` check in `board-view.tsx`:
|
||||
|
||||
```typescript
|
||||
// Correct order in handleDragEnd:
|
||||
if (draggedFeature.status === "backlog") {
|
||||
// ...
|
||||
} else if (draggedFeature.status === "waiting_approval") {
|
||||
// Handle waiting_approval BEFORE skipTests check
|
||||
// because waiting_approval features often have skipTests=true
|
||||
} else if (draggedFeature.skipTests) {
|
||||
// Handle other skipTests features
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices Discovered
|
||||
|
||||
### Testing utilities are critical
|
||||
|
||||
Create comprehensive testing utilities in `tests/utils.ts` to avoid repeating selector logic:
|
||||
|
||||
- `waitForElement` - waits for elements to appear
|
||||
- `waitForElementHidden` - waits for elements to disappear
|
||||
- `setupMockProject` - sets up mock localStorage state
|
||||
- `navigateToBoard` - handles navigation from welcome to board view
|
||||
|
||||
### Always add data-testid attributes
|
||||
|
||||
When implementing features, immediately add `data-testid` attributes to key UI elements. This makes tests more reliable and easier to write.
|
||||
|
||||
### Test timeouts should be generous but not excessive
|
||||
|
||||
- Default timeout: 30s (set in playwright.config.ts)
|
||||
- Element waits: 5-15s for critical elements
|
||||
- Auto mode completion: 10s (accounts for ~4s mock duration)
|
||||
- Don't increase timeouts past 10s for individual operations
|
||||
|
||||
### Mock auto mode timing
|
||||
|
||||
The mock auto mode in `electron.ts` has predictable timing:
|
||||
|
||||
- Total duration: ~2.4 seconds (300+500+300+300+500+500ms)
|
||||
- Plus 1.5s delay before auto-closing modals
|
||||
- Total: ~4 seconds from start to completion
|
||||
|
||||
### Issue: HotkeyButton conflicting with useKeyboardShortcuts
|
||||
|
||||
**Problem:** Adding `HotkeyButton` with a simple key (like "N") to buttons that already had keyboard shortcuts registered via `useKeyboardShortcuts` caused the hotkey to stop working. Both registered duplicate listeners, and the HotkeyButton's `stopPropagation()` call could interfere.
|
||||
**Fix:** When a simple single-key hotkey is already handled by `useKeyboardShortcuts`, set `hotkeyActive={false}` on the `HotkeyButton` so it only displays the indicator badge without registering a duplicate listener:
|
||||
|
||||
```tsx
|
||||
// In views that already use useKeyboardShortcuts for the "N" key:
|
||||
<HotkeyButton
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
hotkey={shortcuts.addFeature}
|
||||
hotkeyActive={false} // <-- Important! Prevents duplicate listener
|
||||
>
|
||||
Add Feature
|
||||
</HotkeyButton>
|
||||
|
||||
// HotkeyButton should only actively listen when it's the sole handler (e.g., Cmd+Enter in dialogs)
|
||||
<HotkeyButton
|
||||
onClick={handleSubmit}
|
||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||
hotkeyActive={isDialogOpen} // Active when dialog is open
|
||||
>
|
||||
Submit
|
||||
</HotkeyButton>
|
||||
```
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,3 +9,6 @@ dist/
|
||||
.next/
|
||||
node_modules
|
||||
.automaker/images/
|
||||
.automaker/
|
||||
/.automaker/*
|
||||
/.automaker/
|
||||
|
||||
77
README.md
77
README.md
@@ -45,16 +45,81 @@ export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-..."
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
## How to Run
|
||||
|
||||
### Development Modes
|
||||
|
||||
Automaker can be run in several modes:
|
||||
|
||||
#### Electron Desktop App (Recommended)
|
||||
|
||||
```bash
|
||||
# Standard development mode
|
||||
npm run dev:electron
|
||||
|
||||
# With DevTools open automatically
|
||||
npm run dev:electron:debug
|
||||
|
||||
# For WSL (Windows Subsystem for Linux)
|
||||
npm run dev:electron:wsl
|
||||
|
||||
# For WSL with GPU acceleration
|
||||
npm run dev:electron:wsl:gpu
|
||||
```
|
||||
|
||||
#### Web Browser Mode
|
||||
|
||||
```bash
|
||||
# Run in web browser (http://localhost:3007)
|
||||
npm run dev:web
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Build Next.js app
|
||||
npm run build
|
||||
|
||||
# Build Electron app for distribution
|
||||
npm run build:electron
|
||||
```
|
||||
|
||||
### Running Production Build
|
||||
|
||||
```bash
|
||||
# Start production Next.js server
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run tests headless
|
||||
npm run test
|
||||
|
||||
# Run tests with browser visible
|
||||
npm run test:headed
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Run ESLint
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Authentication Options
|
||||
|
||||
Automaker supports multiple authentication methods (in order of priority):
|
||||
|
||||
| Method | Environment Variable | Description |
|
||||
|--------|---------------------|-------------|
|
||||
| OAuth Token (env) | `CLAUDE_CODE_OAUTH_TOKEN` | From `claude setup-token` - uses your Claude subscription |
|
||||
| OAuth Token (stored) | — | Stored in app credentials file |
|
||||
| API Key (stored) | — | Anthropic API key stored in app |
|
||||
| API Key (env) | `ANTHROPIC_API_KEY` | Pay-per-use API key |
|
||||
| Method | Environment Variable | Description |
|
||||
| -------------------- | ------------------------- | --------------------------------------------------------- |
|
||||
| OAuth Token (env) | `CLAUDE_CODE_OAUTH_TOKEN` | From `claude setup-token` - uses your Claude subscription |
|
||||
| OAuth Token (stored) | — | Stored in app credentials file |
|
||||
| API Key (stored) | — | Anthropic API key stored in app |
|
||||
| API Key (env) | `ANTHROPIC_API_KEY` | Pay-per-use API key |
|
||||
|
||||
**Recommended:** Use `CLAUDE_CODE_OAUTH_TOKEN` if you have a Claude subscription.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class McpServerFactory {
|
||||
"Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.",
|
||||
{
|
||||
featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"),
|
||||
status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. Use 'backlog' or 'todo' for new features."),
|
||||
status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. For NEW features, ONLY use 'backlog' or 'verified'. NEVER use 'in_progress' for new features - the user will manually start them."),
|
||||
summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."),
|
||||
description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."),
|
||||
category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"),
|
||||
@@ -38,14 +38,16 @@ class McpServerFactory {
|
||||
const feature = features.find((f) => f.id === args.featureId);
|
||||
|
||||
if (!feature) {
|
||||
console.log(`[Feature Creation] Feature ${args.featureId} not found - this might be a new feature being created`);
|
||||
// This might be a new feature - try to proceed anyway
|
||||
console.log(`[Feature Creation] Feature ${args.featureId} not found - this is a new feature being created`);
|
||||
// This is a new feature - enforce backlog status for any non-verified features
|
||||
}
|
||||
|
||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
||||
let finalStatus = args.status;
|
||||
// Convert 'todo' to 'backlog' for consistency, but only for new features
|
||||
if (!feature && finalStatus === "todo") {
|
||||
// For NEW features: Convert 'todo' or 'in_progress' to 'backlog' for consistency
|
||||
// New features should ALWAYS go to backlog first, user must manually start them
|
||||
if (!feature && (finalStatus === "todo" || finalStatus === "in_progress")) {
|
||||
console.log(`[Feature Creation] New feature ${args.featureId} - converting "${finalStatus}" to "backlog" (user must manually start features)`);
|
||||
finalStatus = "backlog";
|
||||
}
|
||||
if (feature && args.status === "verified" && feature.skipTests === true) {
|
||||
|
||||
@@ -390,15 +390,13 @@ class SpecRegenerationService {
|
||||
3. For EACH feature in the implementation_roadmap:
|
||||
- Determine if it's ALREADY IMPLEMENTED (fully or partially)
|
||||
- If fully implemented: Create with status "verified" and note what's done
|
||||
- If partially implemented: Create with status "in_progress" and note remaining work
|
||||
- If not started: Create with status "backlog"
|
||||
- If partially implemented OR not started: Create with status "backlog" and note what still needs to be done
|
||||
|
||||
**IMPORTANT - For each feature you MUST provide:**
|
||||
- **featureId**: A descriptive ID (lowercase, hyphens for spaces). Example: "user-authentication", "budget-tracking"
|
||||
- **status**:
|
||||
- "verified" if feature is fully implemented in the codebase
|
||||
- "in_progress" if partially implemented
|
||||
- "backlog" if not yet started
|
||||
- **status**:
|
||||
- "verified" ONLY if feature is 100% fully implemented in the codebase
|
||||
- "backlog" for ALL features that need ANY work (partial or not started) - the user will manually start these
|
||||
- **description**: A DETAILED description (2-4 sentences) explaining what the feature does, its purpose, and key functionality
|
||||
- **category**: The phase from the roadmap (e.g., "Phase 1: Foundation", "Phase 2: Core Logic", "Phase 3: Polish")
|
||||
- **steps**: An array of 4-8 clear, actionable implementation steps. For verified features, these are what WAS done. For backlog, these are what NEEDS to be done.
|
||||
@@ -407,10 +405,12 @@ class SpecRegenerationService {
|
||||
**Example of analyzing existing code:**
|
||||
If you find NextAuth.js configured in the codebase with working login pages, the user-authentication feature should be "verified" not "backlog".
|
||||
|
||||
**Example of a well-defined feature:**
|
||||
**IMPORTANT: NEVER use "in_progress" status when creating features. Only use "verified" or "backlog".**
|
||||
|
||||
**Example of a well-defined feature (verified - fully complete):**
|
||||
{
|
||||
"featureId": "user-authentication",
|
||||
"status": "verified", // Because we found it's already implemented
|
||||
"status": "verified", // Because we found it's 100% already implemented
|
||||
"description": "Secure user authentication system with email/password login and session management. Already implemented using NextAuth.js with email provider.",
|
||||
"category": "Phase 1: Foundation",
|
||||
"steps": [
|
||||
@@ -422,6 +422,21 @@ If you find NextAuth.js configured in the codebase with working login pages, the
|
||||
"summary": "Authentication implemented with NextAuth.js email provider"
|
||||
}
|
||||
|
||||
**Example of a feature that needs work (backlog):**
|
||||
{
|
||||
"featureId": "user-profile",
|
||||
"status": "backlog", // Needs work - user will manually start this
|
||||
"description": "User profile page where users can view and edit their account settings, change password, and manage preferences.",
|
||||
"category": "Phase 2: Core Features",
|
||||
"steps": [
|
||||
"Create profile page component",
|
||||
"Add form for editing user details",
|
||||
"Implement password change functionality",
|
||||
"Add avatar upload feature"
|
||||
],
|
||||
"summary": "User profile management - needs implementation"
|
||||
}
|
||||
|
||||
**Feature Storage:**
|
||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
||||
Use the UpdateFeatureStatus tool to create features with ALL the fields above.`,
|
||||
@@ -453,13 +468,15 @@ Use the UpdateFeatureStatus tool to create features with ALL the fields above.`,
|
||||
2. **Then, read .automaker/app_spec.txt** to see the implementation roadmap
|
||||
|
||||
3. **For EACH feature in the roadmap, determine its status:**
|
||||
- Is it ALREADY IMPLEMENTED in the codebase? → status: "verified"
|
||||
- Is it PARTIALLY IMPLEMENTED? → status: "in_progress"
|
||||
- Is it NOT STARTED? → status: "backlog"
|
||||
- Is it 100% FULLY IMPLEMENTED in the codebase? → status: "verified"
|
||||
- Is it PARTIALLY IMPLEMENTED or NOT STARTED? → status: "backlog"
|
||||
|
||||
**CRITICAL: NEVER use "in_progress" status. Only use "verified" or "backlog".**
|
||||
The user will manually move features from backlog to in_progress when they want to start working on them.
|
||||
|
||||
4. **Create each feature with UpdateFeatureStatus including ALL fields:**
|
||||
- featureId: Descriptive ID (lowercase, hyphens)
|
||||
- status: "verified", "in_progress", or "backlog" based on your analysis
|
||||
- status: "verified" or "backlog" ONLY (never in_progress)
|
||||
- description: 2-4 sentences explaining the feature
|
||||
- category: The phase name from the roadmap
|
||||
- steps: Array of 4-8 implementation steps
|
||||
@@ -588,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
|
||||
@@ -598,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\``;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -639,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
|
||||
@@ -648,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\`.`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -848,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
|
||||
@@ -861,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\``;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -892,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:**
|
||||
@@ -930,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\`.`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
231
apps/app/src/components/dialogs/file-browser-dialog.tsx
Normal file
231
apps/app/src/components/dialogs/file-browser-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -210,6 +210,7 @@ export function Sidebar() {
|
||||
cycleNextProject,
|
||||
clearProjectHistory,
|
||||
setProjectTheme,
|
||||
setTheme,
|
||||
theme: globalTheme,
|
||||
} = useAppStore();
|
||||
|
||||
@@ -430,12 +431,16 @@ export function Sidebar() {
|
||||
);
|
||||
useAppStore.setState({ projects: updatedProjects });
|
||||
} else {
|
||||
// Create new project
|
||||
// Create new project - check for trashed project with same path first (preserves theme if deleted/recreated)
|
||||
// Then fall back to current effective theme, then global theme
|
||||
const trashedProject = trashedProjects.find((p) => p.path === path);
|
||||
const effectiveTheme = trashedProject?.theme || currentProject?.theme || globalTheme;
|
||||
project = {
|
||||
id: `project-${Date.now()}`,
|
||||
name,
|
||||
path,
|
||||
lastOpened: new Date().toISOString(),
|
||||
theme: effectiveTheme,
|
||||
};
|
||||
addProject(project);
|
||||
}
|
||||
@@ -474,7 +479,7 @@ export function Sidebar() {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [projects, addProject, setCurrentProject]);
|
||||
}, [projects, trashedProjects, addProject, setCurrentProject, currentProject, globalTheme]);
|
||||
|
||||
const handleRestoreProject = useCallback(
|
||||
(projectId: string) => {
|
||||
@@ -963,6 +968,10 @@ export function Sidebar() {
|
||||
value={currentProject.theme || ""}
|
||||
onValueChange={(value) => {
|
||||
if (currentProject) {
|
||||
// If selecting an actual theme (not "Use Global"), also update global
|
||||
if (value !== "") {
|
||||
setTheme(value as any);
|
||||
}
|
||||
setProjectTheme(
|
||||
currentProject.id,
|
||||
value === "" ? null : (value as any)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
File,
|
||||
X,
|
||||
BookOpen,
|
||||
EditIcon,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
useKeyboardShortcuts,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Markdown } from "../ui/markdown";
|
||||
|
||||
interface ContextFile {
|
||||
name: string;
|
||||
@@ -60,6 +63,7 @@ export function ContextView() {
|
||||
);
|
||||
const [newFileContent, setNewFileContent] = useState("");
|
||||
const [isDropHovering, setIsDropHovering] = useState(false);
|
||||
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
||||
|
||||
// Keyboard shortcuts for this view
|
||||
const contextShortcuts: KeyboardShortcut[] = useMemo(
|
||||
@@ -80,6 +84,11 @@ export function ContextView() {
|
||||
return `${currentProject.path}/.automaker/context`;
|
||||
}, [currentProject]);
|
||||
|
||||
const isMarkdownFile = (filename: string): boolean => {
|
||||
const ext = filename.toLowerCase().substring(filename.lastIndexOf("."));
|
||||
return ext === ".md" || ext === ".markdown";
|
||||
};
|
||||
|
||||
// Determine if a file is an image based on extension
|
||||
const isImageFile = (filename: string): boolean => {
|
||||
const imageExtensions = [
|
||||
@@ -151,6 +160,7 @@ export function ContextView() {
|
||||
// Could add a confirmation dialog here
|
||||
}
|
||||
loadFileContent(file);
|
||||
setIsPreviewMode(isMarkdownFile(file.name));
|
||||
};
|
||||
|
||||
// Save current file
|
||||
@@ -448,6 +458,27 @@ export function ContextView() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedFile.type === "text" &&
|
||||
isMarkdownFile(selectedFile.name) && (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size="sm"
|
||||
onClick={() => setIsPreviewMode(!isPreviewMode)}
|
||||
data-testid="toggle-preview-mode"
|
||||
>
|
||||
{isPreviewMode ? (
|
||||
<>
|
||||
<EditIcon className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{selectedFile.type === "text" && (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -484,6 +515,10 @@ export function ContextView() {
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : isPreviewMode ? (
|
||||
<Card className="h-full overflow-auto p-4" data-testid="markdown-preview">
|
||||
<Markdown>{editedContent}</Markdown>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-full overflow-hidden">
|
||||
<textarea
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -82,12 +82,14 @@ export function SettingsView() {
|
||||
// Compute the effective theme for the current project
|
||||
const effectiveTheme = (settingsProject?.theme || theme) as Theme;
|
||||
|
||||
// Handler to set theme - saves to project if one is selected, otherwise to global
|
||||
// Handler to set theme - always updates global theme (user's preference),
|
||||
// and also sets per-project theme if a project is selected
|
||||
const handleSetTheme = (newTheme: typeof theme) => {
|
||||
// Always update global theme so user's preference persists across all projects
|
||||
setTheme(newTheme);
|
||||
// Also set per-project theme if a project is selected
|
||||
if (currentProject) {
|
||||
setProjectTheme(currentProject.id, newTheme);
|
||||
} else {
|
||||
setTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
68
apps/app/src/contexts/file-browser-context.tsx
Normal file
68
apps/app/src/contexts/file-browser-context.tsx
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -636,6 +636,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
name: trashed.name,
|
||||
path: trashed.path,
|
||||
lastOpened: new Date().toISOString(),
|
||||
theme: trashed.theme, // Preserve theme from trashed project
|
||||
};
|
||||
|
||||
set({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,30 +40,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
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
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";
|
||||
|
||||
@@ -370,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 {
|
||||
@@ -422,5 +500,123 @@ export function createFsRoutes(_events: EventEmitter): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Browse directories for file picker
|
||||
// SECURITY: Restricted to home directory, allowed paths, and drive roots on Windows
|
||||
router.post("/browse", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { dirPath } = req.body as { dirPath?: string };
|
||||
const homeDir = 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;
|
||||
};
|
||||
|
||||
// Check if a path is safe to browse
|
||||
const isSafePath = (targetPath: string): boolean => {
|
||||
const resolved = path.resolve(targetPath);
|
||||
const normalizedHome = path.resolve(homeDir);
|
||||
|
||||
// Allow browsing within home directory
|
||||
if (resolved === normalizedHome || resolved.startsWith(normalizedHome + path.sep)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow browsing already-allowed paths
|
||||
if (isPathAllowed(resolved)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// On Windows, allow drive roots for initial navigation
|
||||
if (os.platform() === "win32") {
|
||||
const driveRootMatch = /^[A-Z]:\\$/i.test(resolved);
|
||||
if (driveRootMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// On Unix, allow root for initial navigation (but only list, not read files)
|
||||
if (os.platform() !== "win32" && resolved === "/") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Default to home directory if no path provided
|
||||
const targetPath = dirPath ? path.resolve(dirPath) : homeDir;
|
||||
|
||||
// Security check: validate the path is safe to browse
|
||||
if (!isSafePath(targetPath)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: "Access denied: browsing is restricted to your home directory and allowed project paths",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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 exclude hidden directories
|
||||
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 (only if parent is also safe to browse)
|
||||
const parentPath = path.dirname(targetPath);
|
||||
const hasParent = parentPath !== targetPath && isSafePath(parentPath);
|
||||
|
||||
// Get available drives on Windows
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, "project-spec.md");
|
||||
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", "project-spec.md");
|
||||
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 });
|
||||
|
||||
@@ -382,7 +561,7 @@ async function parseAndCreateFeatures(
|
||||
id: feature.id,
|
||||
title: feature.title,
|
||||
description: feature.description,
|
||||
status: "pending",
|
||||
status: "backlog", // Features go to backlog - user must manually start them
|
||||
priority: feature.priority || 2,
|
||||
complexity: feature.complexity || "moderate",
|
||||
dependencies: feature.dependencies || [],
|
||||
@@ -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 ==========");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user