diff --git a/.claude_settings.json b/.claude_settings.json deleted file mode 100644 index 969f1214..00000000 --- a/.claude_settings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "sandbox": { - "enabled": true, - "autoAllowBashIfSandboxed": true - }, - "permissions": { - "defaultMode": "acceptEdits", - "allow": [ - "Read(./**)", - "Write(./**)", - "Edit(./**)", - "Glob(./**)", - "Grep(./**)", - "Bash(*)", - "mcp__puppeteer__puppeteer_navigate", - "mcp__puppeteer__puppeteer_screenshot", - "mcp__puppeteer__puppeteer_click", - "mcp__puppeteer__puppeteer_fill", - "mcp__puppeteer__puppeteer_select", - "mcp__puppeteer__puppeteer_hover", - "mcp__puppeteer__puppeteer_evaluate" - ] - } -} diff --git a/CHANGELOG_RATE_LIMIT_HANDLING.md b/CHANGELOG_RATE_LIMIT_HANDLING.md deleted file mode 100644 index 35e73b0e..00000000 --- a/CHANGELOG_RATE_LIMIT_HANDLING.md +++ /dev/null @@ -1,134 +0,0 @@ -# Improved Error Handling for Rate Limiting - -## Problem - -When running multiple features concurrently in auto-mode, the Claude API rate limits were being exceeded, resulting in cryptic error messages: - -``` -Error: Claude Code process exited with code 1 -``` - -This error provided no actionable information to users about: - -- What went wrong (rate limit exceeded) -- How long to wait before retrying -- How to prevent it in the future - -## Root Cause - -The Claude Agent SDK was terminating with exit code 1 when hitting rate limits (HTTP 429), but the error details were not being properly surfaced to the user. The error handling code only showed the generic exit code message instead of the actual API error. - -## Solution - -Implemented comprehensive rate limit error handling across the stack: - -### 1. Enhanced Error Classification (libs/utils) - -Added new error type and detection functions: - -- **New error type**: `'rate_limit'` added to `ErrorType` union -- **`isRateLimitError()`**: Detects 429 and rate_limit errors -- **`extractRetryAfter()`**: Extracts retry duration from error messages -- **Updated `classifyError()`**: Includes rate limit classification with retry-after metadata -- **Updated `getUserFriendlyErrorMessage()`**: Provides clear, actionable messages for rate limit errors - -### 2. Improved Claude Provider Error Handling (apps/server) - -Enhanced `ClaudeProvider.executeQuery()` to: - -- Classify all errors using the enhanced error utilities -- Detect rate limit errors specifically -- Provide user-friendly error messages with: - - Clear explanation of the problem (rate limit exceeded) - - Retry-after duration when available - - Actionable tip: reduce `maxConcurrency` in auto-mode -- Preserve original error details for debugging - -### 3. Comprehensive Test Coverage - -Added 8 new tests covering: - -- Rate limit error detection (429, rate_limit keywords) -- Retry-after extraction from various message formats -- Error classification with retry metadata -- User-friendly message generation -- Edge cases (null/undefined, non-rate-limit errors) - -**Total test suite**: 162 tests passing ✅ - -## User-Facing Changes - -### Before - -``` -[AutoMode] Feature touch-gesture-support failed: Error: Claude Code process exited with code 1 -``` - -### After - -``` -[AutoMode] Feature touch-gesture-support failed: Rate limit exceeded (429). Please wait 60 seconds before retrying. - -Tip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits. -``` - -## Benefits - -1. **Clear communication**: Users understand exactly what went wrong -2. **Actionable guidance**: Users know how long to wait and how to prevent future errors -3. **Better debugging**: Original error details preserved for technical investigation -4. **Type safety**: New `isRateLimit` and `retryAfter` fields properly typed in `ErrorInfo` -5. **Comprehensive testing**: All edge cases covered with automated tests - -## Technical Details - -### Files Modified - -- `libs/types/src/error.ts` - Added `'rate_limit'` type and `retryAfter` field -- `libs/utils/src/error-handler.ts` - Added rate limit detection and extraction logic -- `libs/utils/src/index.ts` - Exported new utility functions -- `libs/utils/tests/error-handler.test.ts` - Added 8 new test cases -- `apps/server/src/providers/claude-provider.ts` - Enhanced error handling with user-friendly messages - -### API Changes - -**ErrorInfo interface** (backwards compatible): - -```typescript -interface ErrorInfo { - type: ErrorType; // Now includes 'rate_limit' - message: string; - isAbort: boolean; - isAuth: boolean; - isCancellation: boolean; - isRateLimit: boolean; // NEW - retryAfter?: number; // NEW (seconds to wait) - originalError: unknown; -} -``` - -**New utility functions**: - -```typescript -isRateLimitError(error: unknown): boolean -extractRetryAfter(error: unknown): number | undefined -``` - -## Future Improvements - -This PR lays the groundwork for future enhancements: - -1. **Automatic retry with exponential backoff**: Use `retryAfter` to implement smart retry logic -2. **Global rate limiter**: Track requests to prevent hitting limits proactively -3. **Concurrency auto-adjustment**: Dynamically reduce concurrency when rate limits are detected -4. **User notifications**: Show toast/banner when rate limits are approaching - -## Testing - -Run tests with: - -```bash -npm run test -w @automaker/utils -``` - -All 162 tests pass, including 8 new rate limit tests. diff --git a/docs/migration-plan-nextjs-to-vite.md b/docs/migration-plan-nextjs-to-vite.md deleted file mode 100644 index 7211e1ec..00000000 --- a/docs/migration-plan-nextjs-to-vite.md +++ /dev/null @@ -1,1829 +0,0 @@ -# Migration Plan: Next.js to Vite + Electron + TanStack - -> **Document Version**: 1.1 -> **Date**: December 2025 -> **Status**: Phase 2 Complete - Core Migration Done -> **Branch**: refactor/frontend - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Current Architecture Assessment](#current-architecture-assessment) -3. [Proposed New Architecture](#proposed-new-architecture) -4. [Folder Structure](#folder-structure) -5. [Shared Packages (libs/)](#shared-packages-libs) -6. [Type-Safe Electron Implementation](#type-safe-electron-implementation) -7. [Components Refactoring](#components-refactoring) -8. [Web + Electron Dual Support](#web--electron-dual-support) -9. [Migration Phases](#migration-phases) -10. [Expected Benefits](#expected-benefits) -11. [Risk Mitigation](#risk-mitigation) - ---- - -## Executive Summary - -### Why Migrate? - -Our current Next.js implementation uses **less than 5%** of the framework's capabilities. We're essentially running a static SPA with unnecessary overhead: - -| Next.js Feature | Our Usage | -| ---------------------- | ---------------------------- | -| Server-Side Rendering | ❌ Not used | -| Static Site Generation | ❌ Not used | -| API Routes | ⚠️ Only 2 test endpoints | -| Image Optimization | ❌ Not used | -| Dynamic Routing | ❌ Not used | -| App Router | ⚠️ File structure only | -| Metadata API | ⚠️ Title/description only | -| Static Export | ✅ Used (`output: "export"`) | - -### Migration Benefits - -| Metric | Current (Next.js) | Expected (Vite) | -| ---------------------- | ----------------- | ------------------ | -| Dev server startup | ~8-15s | ~1-3s | -| HMR speed | ~500ms-2s | ~50-100ms | -| Production build | ~45-90s | ~15-30s | -| Bundle overhead | Next.js runtime | None | -| Type safety (Electron) | 0% | 100% | -| Debug capabilities | Limited | Full debug console | - -### Target Stack - -- **Bundler**: Vite -- **Framework**: React 19 -- **Routing**: TanStack Router (file-based) -- **Data Fetching**: TanStack Query -- **State**: Zustand (unchanged) -- **Styling**: Tailwind CSS 4 (unchanged) -- **Desktop**: Electron (TypeScript rewrite) - ---- - -## Current Architecture Assessment - -### Data Flow Diagram - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ELECTRON APP │ -├─────────────────────────────────────────────────────────────────┤ -│ ┌─────────────────┐ HTTP/WS ┌─────────────────┐ │ -│ │ React SPA │ ←──────────────────→ │ Backend Server │ │ -│ │ (Next.js) │ localhost:3008 │ (Express) │ │ -│ │ │ │ │ │ -│ │ • Zustand Store │ │ • AI Providers │ │ -│ │ • 16 Views │ │ • Git/FS Ops │ │ -│ │ • 180+ Comps │ │ • Terminal │ │ -│ └────────┬────────┘ └─────────────────┘ │ -│ │ │ -│ │ IPC (minimal - dialogs/shell only) │ -│ ↓ │ -│ ┌─────────────────┐ │ -│ │ Electron Main │ • File dialogs │ -│ │ (main.js) │ • Shell operations │ -│ │ **NO TYPES** │ • App paths │ -│ └─────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Current Electron Layer Issues - -| Issue | Impact | Solution | -| ----------------------- | --------------------------- | ------------------------ | -| Pure JavaScript | No compile-time safety | Migrate to TypeScript | -| Untyped IPC handlers | Runtime errors | IPC Schema with generics | -| String literal channels | Typos cause silent failures | Const enums | -| No debug tooling | Hard to diagnose issues | Debug console feature | -| Monolithic main.js | Hard to maintain | Modular IPC organization | - -### Current Component Structure Issues - -| View File | Lines | Issue | -| ------------------ | ----- | ---------------------------------- | -| spec-view.tsx | 1,230 | Exceeds 500-line threshold | -| analysis-view.tsx | 1,134 | Exceeds 500-line threshold | -| agent-view.tsx | 916 | Exceeds 500-line threshold | -| welcome-view.tsx | 815 | Exceeds 500-line threshold | -| context-view.tsx | 735 | Exceeds 500-line threshold | -| terminal-view.tsx | 697 | Exceeds 500-line threshold | -| interview-view.tsx | 637 | Exceeds 500-line threshold | -| board-view.tsx | 685 | ✅ Already has subfolder structure | - ---- - -## Proposed New Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ MIGRATED ARCHITECTURE │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ @automaker/app (Vite + React) │ │ -│ ├──────────────────────────────────────────────────────────────────┤ │ -│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │ │ -│ │ │ TanStack │ │ TanStack │ │ Zustand │ │ │ -│ │ │ Router │ │ Query │ │ Store │ │ │ -│ │ │ (file-based) │ │ (data fetch) │ │ (UI state) │ │ │ -│ │ └────────────────┘ └────────────────┘ └────────────────────┘ │ │ -│ │ │ │ -│ │ src/ │ │ -│ │ ├── routes/ # TanStack file-based routes │ │ -│ │ ├── components/ # Refactored per folder-pattern.md │ │ -│ │ ├── hooks/ # React hooks │ │ -│ │ ├── store/ # Zustand stores │ │ -│ │ ├── lib/ # Utilities │ │ -│ │ └── config/ # Configuration │ │ -│ └──────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ HTTP/WS (unchanged) │ Type-Safe IPC │ -│ ↓ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ Electron Layer (TypeScript) │ │ -│ ├──────────────────────────────────────────────────────────────────┤ │ -│ │ electron/ │ │ -│ │ ├── main.ts # Main process entry │ │ -│ │ ├── preload.ts # Context bridge exposure │ │ -│ │ ├── debug-console/ # Debug console feature │ │ -│ │ └── ipc/ # Modular IPC handlers │ │ -│ │ ├── ipc-schema.ts # Type definitions │ │ -│ │ ├── dialog/ # File dialogs │ │ -│ │ ├── shell/ # Shell operations │ │ -│ │ └── server/ # Server management │ │ -│ └──────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────────────┐ │ -│ │ @automaker/server (unchanged) │ │ -│ └──────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ SHARED PACKAGES (libs/) │ -├─────────────────────────────────────────────────────────────────────────┤ -│ @automaker/types # API contracts, model definitions │ -│ @automaker/utils # Shared utilities (error handling, etc.) │ -│ @automaker/platform # OS-specific utilities, path handling │ -│ @automaker/model-resolver # Model string resolution │ -│ @automaker/ipc-types # IPC channel type definitions │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Folder Structure - -### apps/ui/ (After Migration) - -``` -apps/ui/ -├── electron/ # Electron main process (TypeScript) -│ ├── main.ts # Main entry point -│ ├── preload.ts # Context bridge -│ ├── tsconfig.json # Electron-specific TS config -│ ├── debug-console/ -│ │ ├── debug-console.html -│ │ ├── debug-console-preload.ts -│ │ └── debug-mode.ts -│ ├── ipc/ -│ │ ├── ipc-schema.ts # Central type definitions -│ │ ├── context-exposer.ts # Exposes all contexts to renderer -│ │ ├── listeners-register.ts # Registers all main process handlers -│ │ ├── dialog/ -│ │ │ ├── dialog-channels.ts # Channel constants -│ │ │ ├── dialog-context.ts # Preload exposure -│ │ │ └── dialog-listeners.ts # Main process handlers -│ │ ├── shell/ -│ │ │ ├── shell-channels.ts -│ │ │ ├── shell-context.ts -│ │ │ └── shell-listeners.ts -│ │ ├── app-info/ -│ │ │ ├── app-info-channels.ts -│ │ │ ├── app-info-context.ts -│ │ │ └── app-info-listeners.ts -│ │ └── server/ -│ │ ├── server-channels.ts -│ │ ├── server-context.ts -│ │ └── server-listeners.ts -│ └── helpers/ -│ ├── server-manager.ts # Backend server spawn/health -│ ├── static-server.ts # Production static file server -│ ├── window-helpers.ts # Window utilities -│ └── window-registry.ts # Multi-window tracking -│ -├── src/ -│ ├── routes/ # TanStack Router (file-based) -│ │ ├── __root.tsx # Root layout -│ │ ├── index.tsx # Welcome/home (default route) -│ │ ├── board.tsx # Board view -│ │ ├── agent.tsx # Agent view -│ │ ├── settings.tsx # Settings view -│ │ ├── setup.tsx # Setup view -│ │ ├── terminal.tsx # Terminal view -│ │ ├── spec.tsx # Spec view -│ │ ├── context.tsx # Context view -│ │ ├── profiles.tsx # Profiles view -│ │ ├── interview.tsx # Interview view -│ │ ├── wiki.tsx # Wiki view -│ │ ├── analysis.tsx # Analysis view -│ │ └── agent-tools.tsx # Agent tools view -│ │ -│ ├── components/ # Refactored per folder-pattern.md -│ │ ├── ui/ # Global UI primitives (unchanged) -│ │ ├── layout/ -│ │ │ ├── sidebar.tsx -│ │ │ ├── base-layout.tsx -│ │ │ └── index.ts -│ │ ├── dialogs/ # Global dialogs -│ │ │ ├── index.ts -│ │ │ ├── new-project-modal.tsx -│ │ │ ├── workspace-picker-modal.tsx -│ │ │ └── file-browser-dialog.tsx -│ │ └── views/ # Complex view components -│ │ ├── board-view/ # ✅ Already structured -│ │ ├── settings-view/ # Needs dialogs reorganization -│ │ ├── setup-view/ # ✅ Already structured -│ │ ├── profiles-view/ # ✅ Already structured -│ │ ├── agent-view/ # NEW: needs subfolder -│ │ │ ├── components/ -│ │ │ │ ├── index.ts -│ │ │ │ ├── message-list.tsx -│ │ │ │ ├── message-input.tsx -│ │ │ │ └── session-sidebar.tsx -│ │ │ ├── dialogs/ -│ │ │ │ ├── index.ts -│ │ │ │ ├── delete-session-dialog.tsx -│ │ │ │ └── delete-all-archived-dialog.tsx -│ │ │ └── hooks/ -│ │ │ ├── index.ts -│ │ │ └── use-agent-state.ts -│ │ ├── spec-view/ # NEW: needs subfolder (1230 lines!) -│ │ ├── analysis-view/ # NEW: needs subfolder (1134 lines!) -│ │ ├── context-view/ # NEW: needs subfolder -│ │ ├── welcome-view/ # NEW: needs subfolder -│ │ ├── interview-view/ # NEW: needs subfolder -│ │ └── terminal-view/ # Expand existing -│ │ -│ ├── hooks/ # Global hooks -│ ├── store/ # Zustand stores -│ ├── lib/ # Utilities -│ ├── config/ # Configuration -│ ├── contexts/ # React contexts -│ ├── types/ # Type definitions -│ ├── App.tsx # Root component -│ ├── renderer.ts # Vite entry point -│ └── routeTree.gen.ts # Generated by TanStack Router -│ -├── index.html # Vite HTML entry -├── vite.config.mts # Vite configuration -├── tsconfig.json # TypeScript config (renderer) -├── package.json -└── tailwind.config.ts -``` - ---- - -## Shared Packages (libs/) - -### Package Overview - -``` -libs/ -├── @automaker/types # API contracts, model definitions -├── @automaker/utils # General utilities (error handling, logger) -├── @automaker/platform # OS-specific utilities, path handling -├── @automaker/model-resolver # Model string resolution -└── @automaker/ipc-types # IPC channel type definitions -``` - -### @automaker/types - -Shared type definitions for API contracts between frontend and backend. - -``` -libs/types/ -├── src/ -│ ├── api.ts # API response types -│ ├── models.ts # ModelDefinition, ProviderStatus -│ ├── features.ts # Feature, FeatureStatus, Priority -│ ├── sessions.ts # Session, Message types -│ ├── agent.ts # Agent types -│ ├── git.ts # Git operation types -│ ├── worktree.ts # Worktree types -│ └── index.ts # Barrel export -├── package.json -└── tsconfig.json -``` - -```typescript -// libs/types/src/models.ts -export interface ModelDefinition { - id: string; - name: string; - provider: ProviderType; - contextWindow: number; - maxOutputTokens: number; - capabilities: ModelCapabilities; -} - -export interface ModelCapabilities { - vision: boolean; - toolUse: boolean; - streaming: boolean; - computerUse: boolean; -} - -export type ProviderType = 'claude' | 'openai' | 'gemini' | 'ollama'; -``` - -### @automaker/utils - -General utilities shared between frontend and backend. - -``` -libs/utils/ -├── src/ -│ ├── error-handler.ts # Error classification & user-friendly messages -│ ├── logger.ts # Logging utilities -│ ├── conversation-utils.ts # Message formatting & history -│ ├── image-utils.ts # Image processing utilities -│ ├── string-utils.ts # String manipulation helpers -│ └── index.ts -├── package.json -└── tsconfig.json -``` - -```typescript -// libs/utils/src/error-handler.ts -export type ErrorType = - | 'authentication' - | 'rate_limit' - | 'network' - | 'validation' - | 'not_found' - | 'server' - | 'unknown'; - -export interface ErrorInfo { - type: ErrorType; - message: string; - userMessage: string; - retryable: boolean; - statusCode?: number; -} - -export function classifyError(error: unknown): ErrorInfo; -export function getUserFriendlyErrorMessage(error: unknown): string; -export function isAbortError(error: unknown): boolean; -export function isAuthenticationError(error: unknown): boolean; -export function isRateLimitError(error: unknown): boolean; -``` - -### @automaker/platform - -**OS-specific utilities, path handling, and cross-platform helpers.** - -``` -libs/platform/ -├── src/ -│ ├── paths/ -│ │ ├── index.ts # Path utilities barrel export -│ │ ├── path-resolver.ts # Cross-platform path resolution -│ │ ├── path-constants.ts # Common path constants -│ │ └── path-validator.ts # Path validation utilities -│ ├── os/ -│ │ ├── index.ts # OS utilities barrel export -│ │ ├── platform-info.ts # Platform detection & info -│ │ ├── shell-commands.ts # OS-specific shell commands -│ │ └── env-utils.ts # Environment variable utilities -│ ├── fs/ -│ │ ├── index.ts # FS utilities barrel export -│ │ ├── safe-fs.ts # Symlink-safe file operations -│ │ ├── temp-files.ts # Temporary file handling -│ │ └── permissions.ts # File permission utilities -│ └── index.ts # Main barrel export -├── package.json -└── tsconfig.json -``` - -```typescript -// libs/platform/src/paths/path-resolver.ts -import path from 'path'; - -/** - * Platform-aware path separator - */ -export const SEP = path.sep; - -/** - * Normalizes a path to use the correct separator for the current OS - */ -export function normalizePath(inputPath: string): string { - return inputPath.replace(/[/\\]/g, SEP); -} - -/** - * Converts a path to POSIX format (forward slashes) - * Useful for consistent storage/comparison - */ -export function toPosixPath(inputPath: string): string { - return inputPath.replace(/\\/g, '/'); -} - -/** - * Converts a path to Windows format (backslashes) - */ -export function toWindowsPath(inputPath: string): string { - return inputPath.replace(/\//g, '\\'); -} - -/** - * Resolves a path relative to a base, handling platform differences - */ -export function resolvePath(basePath: string, ...segments: string[]): string { - return path.resolve(basePath, ...segments); -} - -/** - * Gets the relative path from one location to another - */ -export function getRelativePath(from: string, to: string): string { - return path.relative(from, to); -} - -/** - * Joins path segments with proper platform separator - */ -export function joinPath(...segments: string[]): string { - return path.join(...segments); -} - -/** - * Extracts directory name from a path - */ -export function getDirname(filePath: string): string { - return path.dirname(filePath); -} - -/** - * Extracts filename from a path - */ -export function getBasename(filePath: string, ext?: string): string { - return path.basename(filePath, ext); -} - -/** - * Extracts file extension from a path - */ -export function getExtension(filePath: string): string { - return path.extname(filePath); -} - -/** - * Checks if a path is absolute - */ -export function isAbsolutePath(inputPath: string): boolean { - return path.isAbsolute(inputPath); -} - -/** - * Ensures a path is absolute, resolving relative to cwd if needed - */ -export function ensureAbsolutePath(inputPath: string, basePath?: string): string { - if (isAbsolutePath(inputPath)) { - return inputPath; - } - return resolvePath(basePath || process.cwd(), inputPath); -} -``` - -```typescript -// libs/platform/src/paths/path-constants.ts -import path from 'path'; -import os from 'os'; - -/** - * Common system paths - */ -export const SYSTEM_PATHS = { - /** User's home directory */ - home: os.homedir(), - - /** System temporary directory */ - temp: os.tmpdir(), - - /** Current working directory */ - cwd: process.cwd(), -} as const; - -/** - * Gets the appropriate app data directory for the current platform - */ -export function getAppDataPath(appName: string): string { - const platform = process.platform; - - switch (platform) { - case 'win32': - return path.join( - process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), - appName - ); - case 'darwin': - return path.join(os.homedir(), 'Library', 'Application Support', appName); - default: // Linux and others - return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), appName); - } -} - -/** - * Gets the appropriate cache directory for the current platform - */ -export function getCachePath(appName: string): string { - const platform = process.platform; - - switch (platform) { - case 'win32': - return path.join( - process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), - appName, - 'Cache' - ); - case 'darwin': - return path.join(os.homedir(), 'Library', 'Caches', appName); - default: - return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), appName); - } -} - -/** - * Gets the appropriate logs directory for the current platform - */ -export function getLogsPath(appName: string): string { - const platform = process.platform; - - switch (platform) { - case 'win32': - return path.join( - process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), - appName, - 'Logs' - ); - case 'darwin': - return path.join(os.homedir(), 'Library', 'Logs', appName); - default: - return path.join( - process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), - appName, - 'logs' - ); - } -} - -/** - * Gets the user's Documents directory - */ -export function getDocumentsPath(): string { - const platform = process.platform; - - switch (platform) { - case 'win32': - return process.env.USERPROFILE - ? path.join(process.env.USERPROFILE, 'Documents') - : path.join(os.homedir(), 'Documents'); - case 'darwin': - return path.join(os.homedir(), 'Documents'); - default: - return process.env.XDG_DOCUMENTS_DIR || path.join(os.homedir(), 'Documents'); - } -} - -/** - * Gets the user's Desktop directory - */ -export function getDesktopPath(): string { - const platform = process.platform; - - switch (platform) { - case 'win32': - return process.env.USERPROFILE - ? path.join(process.env.USERPROFILE, 'Desktop') - : path.join(os.homedir(), 'Desktop'); - case 'darwin': - return path.join(os.homedir(), 'Desktop'); - default: - return process.env.XDG_DESKTOP_DIR || path.join(os.homedir(), 'Desktop'); - } -} -``` - -```typescript -// libs/platform/src/paths/path-validator.ts -import path from 'path'; -import { isAbsolutePath } from './path-resolver'; - -/** - * Characters that are invalid in file/directory names on Windows - */ -const WINDOWS_INVALID_CHARS = /[<>:"|?*\x00-\x1f]/g; - -/** - * Reserved names on Windows (case-insensitive) - */ -const WINDOWS_RESERVED_NAMES = [ - 'CON', - 'PRN', - 'AUX', - 'NUL', - 'COM1', - 'COM2', - 'COM3', - 'COM4', - 'COM5', - 'COM6', - 'COM7', - 'COM8', - 'COM9', - 'LPT1', - 'LPT2', - 'LPT3', - 'LPT4', - 'LPT5', - 'LPT6', - 'LPT7', - 'LPT8', - 'LPT9', -]; - -export interface PathValidationResult { - valid: boolean; - errors: string[]; - sanitized?: string; -} - -/** - * Validates a filename for the current platform - */ -export function validateFilename(filename: string): PathValidationResult { - const errors: string[] = []; - - if (!filename || filename.trim().length === 0) { - return { valid: false, errors: ['Filename cannot be empty'] }; - } - - // Check for path separators (filename shouldn't be a path) - if (filename.includes('/') || filename.includes('\\')) { - errors.push('Filename cannot contain path separators'); - } - - // Platform-specific checks - if (process.platform === 'win32') { - if (WINDOWS_INVALID_CHARS.test(filename)) { - errors.push('Filename contains invalid characters for Windows'); - } - - const nameWithoutExt = filename.split('.')[0].toUpperCase(); - if (WINDOWS_RESERVED_NAMES.includes(nameWithoutExt)) { - errors.push(`"${nameWithoutExt}" is a reserved name on Windows`); - } - - if (filename.endsWith(' ') || filename.endsWith('.')) { - errors.push('Filename cannot end with a space or period on Windows'); - } - } - - // Check length - if (filename.length > 255) { - errors.push('Filename exceeds maximum length of 255 characters'); - } - - return { - valid: errors.length === 0, - errors, - sanitized: errors.length > 0 ? sanitizeFilename(filename) : filename, - }; -} - -/** - * Sanitizes a filename for cross-platform compatibility - */ -export function sanitizeFilename(filename: string): string { - let sanitized = filename.replace(WINDOWS_INVALID_CHARS, '_').replace(/[/\\]/g, '_').trim(); - - // Handle Windows reserved names - const nameWithoutExt = sanitized.split('.')[0].toUpperCase(); - if (WINDOWS_RESERVED_NAMES.includes(nameWithoutExt)) { - sanitized = '_' + sanitized; - } - - // Remove trailing spaces and periods (Windows) - sanitized = sanitized.replace(/[\s.]+$/, ''); - - // Ensure not empty - if (!sanitized) { - sanitized = 'unnamed'; - } - - // Truncate if too long - if (sanitized.length > 255) { - const ext = path.extname(sanitized); - const name = path.basename(sanitized, ext); - sanitized = name.slice(0, 255 - ext.length) + ext; - } - - return sanitized; -} - -/** - * Validates a full path for the current platform - */ -export function validatePath(inputPath: string): PathValidationResult { - const errors: string[] = []; - - if (!inputPath || inputPath.trim().length === 0) { - return { valid: false, errors: ['Path cannot be empty'] }; - } - - // Check total path length - const maxPathLength = process.platform === 'win32' ? 260 : 4096; - if (inputPath.length > maxPathLength) { - errors.push(`Path exceeds maximum length of ${maxPathLength} characters`); - } - - // Validate each segment - const segments = inputPath.split(/[/\\]/).filter(Boolean); - for (const segment of segments) { - // Skip drive letters on Windows - if (process.platform === 'win32' && /^[a-zA-Z]:$/.test(segment)) { - continue; - } - - const segmentValidation = validateFilename(segment); - if (!segmentValidation.valid) { - errors.push(...segmentValidation.errors.map((e) => `Segment "${segment}": ${e}`)); - } - } - - return { - valid: errors.length === 0, - errors, - }; -} - -/** - * Checks if a path is within a base directory (prevents directory traversal) - */ -export function isPathWithin(childPath: string, parentPath: string): boolean { - const resolvedChild = path.resolve(childPath); - const resolvedParent = path.resolve(parentPath); - - return resolvedChild.startsWith(resolvedParent + path.sep) || resolvedChild === resolvedParent; -} -``` - -```typescript -// libs/platform/src/os/platform-info.ts -import os from 'os'; - -export type Platform = 'windows' | 'macos' | 'linux' | 'unknown'; -export type Architecture = 'x64' | 'arm64' | 'ia32' | 'unknown'; - -export interface PlatformInfo { - platform: Platform; - arch: Architecture; - release: string; - hostname: string; - username: string; - cpus: number; - totalMemory: number; - freeMemory: number; - isWsl: boolean; - isDocker: boolean; -} - -/** - * Gets the normalized platform name - */ -export function getPlatform(): Platform { - switch (process.platform) { - case 'win32': - return 'windows'; - case 'darwin': - return 'macos'; - case 'linux': - return 'linux'; - default: - return 'unknown'; - } -} - -/** - * Gets the normalized architecture - */ -export function getArchitecture(): Architecture { - switch (process.arch) { - case 'x64': - return 'x64'; - case 'arm64': - return 'arm64'; - case 'ia32': - return 'ia32'; - default: - return 'unknown'; - } -} - -/** - * Checks if running on Windows - */ -export function isWindows(): boolean { - return process.platform === 'win32'; -} - -/** - * Checks if running on macOS - */ -export function isMacOS(): boolean { - return process.platform === 'darwin'; -} - -/** - * Checks if running on Linux - */ -export function isLinux(): boolean { - return process.platform === 'linux'; -} - -/** - * Checks if running in WSL (Windows Subsystem for Linux) - */ -export function isWsl(): boolean { - if (process.platform !== 'linux') return false; - - try { - const release = os.release().toLowerCase(); - return release.includes('microsoft') || release.includes('wsl'); - } catch { - return false; - } -} - -/** - * Checks if running in Docker container - */ -export function isDocker(): boolean { - try { - const fs = require('fs'); - return ( - fs.existsSync('/.dockerenv') || - (fs.existsSync('/proc/1/cgroup') && - fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker')) - ); - } catch { - return false; - } -} - -/** - * Gets comprehensive platform information - */ -export function getPlatformInfo(): PlatformInfo { - return { - platform: getPlatform(), - arch: getArchitecture(), - release: os.release(), - hostname: os.hostname(), - username: os.userInfo().username, - cpus: os.cpus().length, - totalMemory: os.totalmem(), - freeMemory: os.freemem(), - isWsl: isWsl(), - isDocker: isDocker(), - }; -} - -/** - * Gets the appropriate line ending for the current platform - */ -export function getLineEnding(): string { - return isWindows() ? '\r\n' : '\n'; -} - -/** - * Normalizes line endings to the current platform - */ -export function normalizeLineEndings(text: string): string { - const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - return isWindows() ? normalized.replace(/\n/g, '\r\n') : normalized; -} -``` - -```typescript -// libs/platform/src/os/shell-commands.ts -import { isWindows, isMacOS } from './platform-info'; - -export interface ShellCommand { - command: string; - args: string[]; -} - -/** - * Gets the appropriate command to open a file/URL with default application - */ -export function getOpenCommand(target: string): ShellCommand { - if (isWindows()) { - return { command: 'cmd', args: ['/c', 'start', '', target] }; - } else if (isMacOS()) { - return { command: 'open', args: [target] }; - } else { - return { command: 'xdg-open', args: [target] }; - } -} - -/** - * Gets the appropriate command to reveal a file in file manager - */ -export function getRevealCommand(filePath: string): ShellCommand { - if (isWindows()) { - return { command: 'explorer', args: ['/select,', filePath] }; - } else if (isMacOS()) { - return { command: 'open', args: ['-R', filePath] }; - } else { - // Linux: try multiple file managers - return { command: 'xdg-open', args: [require('path').dirname(filePath)] }; - } -} - -/** - * Gets the default shell for the current platform - */ -export function getDefaultShell(): string { - if (isWindows()) { - return process.env.COMSPEC || 'cmd.exe'; - } - return process.env.SHELL || '/bin/sh'; -} - -/** - * Gets shell-specific arguments for running a command - */ -export function getShellArgs(command: string): ShellCommand { - if (isWindows()) { - return { command: 'cmd.exe', args: ['/c', command] }; - } - return { command: '/bin/sh', args: ['-c', command] }; -} - -/** - * Escapes a string for safe use in shell commands - */ -export function escapeShellArg(arg: string): string { - if (isWindows()) { - // Windows cmd.exe escaping - return `"${arg.replace(/"/g, '""')}"`; - } - // POSIX shell escaping - return `'${arg.replace(/'/g, "'\\''")}'`; -} -``` - -```typescript -// libs/platform/src/os/env-utils.ts -import { isWindows } from './platform-info'; - -/** - * Gets an environment variable with a fallback - */ -export function getEnv(key: string, fallback?: string): string | undefined { - return process.env[key] ?? fallback; -} - -/** - * Gets an environment variable, throwing if not set - */ -export function requireEnv(key: string): string { - const value = process.env[key]; - if (value === undefined) { - throw new Error(`Required environment variable "${key}" is not set`); - } - return value; -} - -/** - * Parses a boolean environment variable - */ -export function getBoolEnv(key: string, fallback = false): boolean { - const value = process.env[key]; - if (value === undefined) return fallback; - return ['true', '1', 'yes', 'on'].includes(value.toLowerCase()); -} - -/** - * Parses a numeric environment variable - */ -export function getNumericEnv(key: string, fallback: number): number { - const value = process.env[key]; - if (value === undefined) return fallback; - const parsed = parseInt(value, 10); - return isNaN(parsed) ? fallback : parsed; -} - -/** - * Expands environment variables in a string - * Supports both $VAR and ${VAR} syntax, plus %VAR% on Windows - */ -export function expandEnvVars(input: string): string { - let result = input; - - // Expand ${VAR} syntax - result = result.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || ''); - - // Expand $VAR syntax (not followed by another word char) - result = result.replace( - /\$([A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_])/g, - (_, name) => process.env[name] || '' - ); - - // Expand %VAR% syntax (Windows) - if (isWindows()) { - result = result.replace(/%([^%]+)%/g, (_, name) => process.env[name] || ''); - } - - return result; -} - -/** - * Gets the PATH environment variable as an array - */ -export function getPathEntries(): string[] { - const pathVar = process.env.PATH || process.env.Path || ''; - const separator = isWindows() ? ';' : ':'; - return pathVar.split(separator).filter(Boolean); -} - -/** - * Checks if a command is available in PATH - */ -export function isCommandInPath(command: string): boolean { - const pathEntries = getPathEntries(); - const extensions = isWindows() ? (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';') : ['']; - const path = require('path'); - const fs = require('fs'); - - for (const dir of pathEntries) { - for (const ext of extensions) { - const fullPath = path.join(dir, command + ext); - try { - fs.accessSync(fullPath, fs.constants.X_OK); - return true; - } catch { - // Continue searching - } - } - } - - return false; -} -``` - -```typescript -// libs/platform/src/fs/safe-fs.ts -import fs from 'fs'; -import path from 'path'; - -/** - * Safely reads a file, following symlinks but preventing escape from base directory - */ -export async function safeReadFile( - filePath: string, - basePath: string, - encoding: BufferEncoding = 'utf8' -): Promise { - const resolvedPath = path.resolve(filePath); - const resolvedBase = path.resolve(basePath); - - // Resolve symlinks - const realPath = await fs.promises.realpath(resolvedPath); - const realBase = await fs.promises.realpath(resolvedBase); - - // Ensure resolved path is within base - if (!realPath.startsWith(realBase + path.sep) && realPath !== realBase) { - throw new Error(`Path "${filePath}" resolves outside of allowed directory`); - } - - return fs.promises.readFile(realPath, encoding); -} - -/** - * Safely writes a file, preventing writes outside base directory - */ -export async function safeWriteFile( - filePath: string, - basePath: string, - content: string -): Promise { - const resolvedPath = path.resolve(filePath); - const resolvedBase = path.resolve(basePath); - - // Ensure path is within base before any symlink resolution - if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) { - throw new Error(`Path "${filePath}" is outside of allowed directory`); - } - - // Check parent directory exists and is within base - const parentDir = path.dirname(resolvedPath); - - try { - const realParent = await fs.promises.realpath(parentDir); - const realBase = await fs.promises.realpath(resolvedBase); - - if (!realParent.startsWith(realBase + path.sep) && realParent !== realBase) { - throw new Error(`Parent directory resolves outside of allowed directory`); - } - } catch (error) { - // Parent doesn't exist, that's OK - we'll create it - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - throw error; - } - } - - await fs.promises.mkdir(path.dirname(resolvedPath), { recursive: true }); - await fs.promises.writeFile(resolvedPath, content, 'utf8'); -} - -/** - * Checks if a path exists and is accessible - */ -export async function pathExists(filePath: string): Promise { - try { - await fs.promises.access(filePath); - return true; - } catch { - return false; - } -} - -/** - * Gets file stats, returning null if file doesn't exist - */ -export async function safeStat(filePath: string): Promise { - try { - return await fs.promises.stat(filePath); - } catch { - return null; - } -} - -/** - * Recursively removes a directory - */ -export async function removeDirectory(dirPath: string): Promise { - await fs.promises.rm(dirPath, { recursive: true, force: true }); -} - -/** - * Copies a file or directory - */ -export async function copy(src: string, dest: string): Promise { - const stats = await fs.promises.stat(src); - - if (stats.isDirectory()) { - await fs.promises.mkdir(dest, { recursive: true }); - const entries = await fs.promises.readdir(src, { withFileTypes: true }); - - for (const entry of entries) { - await copy(path.join(src, entry.name), path.join(dest, entry.name)); - } - } else { - await fs.promises.copyFile(src, dest); - } -} -``` - -```typescript -// libs/platform/src/index.ts -// Main barrel export - -// Path utilities -export * from './paths/path-resolver'; -export * from './paths/path-constants'; -export * from './paths/path-validator'; - -// OS utilities -export * from './os/platform-info'; -export * from './os/shell-commands'; -export * from './os/env-utils'; - -// File system utilities -export * from './fs/safe-fs'; -``` - -### @automaker/model-resolver - -Model string resolution shared between frontend and backend. - -``` -libs/model-resolver/ -├── src/ -│ ├── model-map.ts # CLAUDE_MODEL_MAP, DEFAULT_MODELS -│ ├── resolver.ts # resolveModelString, getEffectiveModel -│ └── index.ts -├── package.json -└── tsconfig.json -``` - -### @automaker/ipc-types - -IPC channel type definitions for type-safe Electron communication. - -``` -libs/ipc-types/ -├── src/ -│ ├── schema.ts # IPCSchema interface -│ ├── channels.ts # Channel constant enums -│ ├── helpers.ts # Type helper functions -│ └── index.ts -├── package.json -└── tsconfig.json -``` - ---- - -## Type-Safe Electron Implementation - -### IPC Schema Definition - -```typescript -// electron/ipc/ipc-schema.ts -import type { OpenDialogOptions, SaveDialogOptions } from 'electron'; - -// Dialog result types -export interface DialogResult { - canceled: boolean; - filePaths?: string[]; - filePath?: string; - data?: T; -} - -// App path names (from Electron) -export type AppPathName = - | 'home' - | 'appData' - | 'userData' - | 'sessionData' - | 'temp' - | 'exe' - | 'module' - | 'desktop' - | 'documents' - | 'downloads' - | 'music' - | 'pictures' - | 'videos' - | 'recent' - | 'logs' - | 'crashDumps'; - -// Complete IPC Schema with request/response types -export interface IPCSchema { - // Dialog operations - 'dialog:openDirectory': { - request: Partial; - response: DialogResult; - }; - 'dialog:openFile': { - request: Partial; - response: DialogResult; - }; - 'dialog:saveFile': { - request: Partial; - response: DialogResult; - }; - - // Shell operations - 'shell:openExternal': { - request: { url: string }; - response: { success: boolean; error?: string }; - }; - 'shell:openPath': { - request: { path: string }; - response: { success: boolean; error?: string }; - }; - - // App info - 'app:getPath': { - request: { name: AppPathName }; - response: string; - }; - 'app:getVersion': { - request: void; - response: string; - }; - 'app:isPackaged': { - request: void; - response: boolean; - }; - - // Server management - 'server:getUrl': { - request: void; - response: string; - }; - - // Connection test - ping: { - request: void; - response: 'pong'; - }; - - // Debug console - 'debug:log': { - request: { - level: DebugLogLevel; - category: DebugCategory; - message: string; - args: unknown[]; - }; - response: void; - }; -} - -export type DebugLogLevel = 'info' | 'warn' | 'error' | 'debug' | 'success'; -export type DebugCategory = - | 'general' - | 'ipc' - | 'route' - | 'network' - | 'perf' - | 'state' - | 'lifecycle' - | 'updater'; - -// Type extractors -export type IPCChannel = keyof IPCSchema; -export type IPCRequest = IPCSchema[T]['request']; -export type IPCResponse = IPCSchema[T]['response']; -``` - -### Modular IPC Organization - -```typescript -// electron/ipc/dialog/dialog-channels.ts -export const DIALOG_CHANNELS = { - OPEN_DIRECTORY: 'dialog:openDirectory', - OPEN_FILE: 'dialog:openFile', - SAVE_FILE: 'dialog:saveFile', -} as const; - -// electron/ipc/dialog/dialog-context.ts -import { contextBridge, ipcRenderer } from 'electron'; -import { DIALOG_CHANNELS } from './dialog-channels'; -import type { IPCRequest, IPCResponse } from '../ipc-schema'; - -export function exposeDialogContext(): void { - contextBridge.exposeInMainWorld('dialogAPI', { - openDirectory: (options?: IPCRequest<'dialog:openDirectory'>) => - ipcRenderer.invoke(DIALOG_CHANNELS.OPEN_DIRECTORY, options), - - openFile: (options?: IPCRequest<'dialog:openFile'>) => - ipcRenderer.invoke(DIALOG_CHANNELS.OPEN_FILE, options), - - saveFile: (options?: IPCRequest<'dialog:saveFile'>) => - ipcRenderer.invoke(DIALOG_CHANNELS.SAVE_FILE, options), - }); -} - -// electron/ipc/dialog/dialog-listeners.ts -import { ipcMain, dialog, BrowserWindow } from 'electron'; -import { DIALOG_CHANNELS } from './dialog-channels'; -import type { IPCRequest, IPCResponse } from '../ipc-schema'; -import { debugLog } from '../../helpers/debug-mode'; - -export function addDialogEventListeners(mainWindow: BrowserWindow): void { - ipcMain.handle( - DIALOG_CHANNELS.OPEN_DIRECTORY, - async (_, options: IPCRequest<'dialog:openDirectory'> = {}) => { - debugLog.ipc(`OPEN_DIRECTORY called with options: ${JSON.stringify(options)}`); - - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory', 'createDirectory'], - ...options, - }); - - debugLog.ipc( - `OPEN_DIRECTORY result: canceled=${result.canceled}, paths=${result.filePaths.length}` - ); - - return { - canceled: result.canceled, - filePaths: result.filePaths, - } satisfies IPCResponse<'dialog:openDirectory'>; - } - ); - - ipcMain.handle( - DIALOG_CHANNELS.OPEN_FILE, - async (_, options: IPCRequest<'dialog:openFile'> = {}) => { - debugLog.ipc(`OPEN_FILE called`); - - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openFile'], - ...options, - }); - - return { - canceled: result.canceled, - filePaths: result.filePaths, - } satisfies IPCResponse<'dialog:openFile'>; - } - ); - - ipcMain.handle( - DIALOG_CHANNELS.SAVE_FILE, - async (_, options: IPCRequest<'dialog:saveFile'> = {}) => { - debugLog.ipc(`SAVE_FILE called`); - - const result = await dialog.showSaveDialog(mainWindow, options); - - return { - canceled: result.canceled, - filePath: result.filePath, - } satisfies IPCResponse<'dialog:saveFile'>; - } - ); -} -``` - ---- - -## Components Refactoring - -### Priority Matrix - -| Priority | View | Lines | Action Required | -| -------- | -------------- | ----- | ------------------------------------------------------ | -| 🔴 P0 | spec-view | 1,230 | Create subfolder with components/, dialogs/, hooks/ | -| 🔴 P0 | analysis-view | 1,134 | Create subfolder with components/, dialogs/, hooks/ | -| 🔴 P0 | agent-view | 916 | Create subfolder, extract message list, input, sidebar | -| 🟡 P1 | welcome-view | 815 | Create subfolder, extract sections | -| 🟡 P1 | context-view | 735 | Create subfolder, extract components | -| 🟡 P1 | terminal-view | 697 | Expand existing subfolder | -| 🟡 P1 | interview-view | 637 | Create subfolder | -| 🟢 P2 | settings-view | 178 | Move dialogs from components/ to dialogs/ | -| ✅ Done | board-view | 685 | Already properly structured | -| ✅ Done | setup-view | 144 | Already properly structured | -| ✅ Done | profiles-view | 300 | Already properly structured | - -### Immediate Dialog Reorganization - -```bash -# Settings-view: Move dialogs to proper location -mv settings-view/components/keyboard-map-dialog.tsx → settings-view/dialogs/ -mv settings-view/components/delete-project-dialog.tsx → settings-view/dialogs/ - -# Root components: Organize global dialogs -mv components/dialogs/board-background-modal.tsx → board-view/dialogs/ - -# Agent-related dialogs: Move to agent-view -mv components/delete-session-dialog.tsx → agent-view/dialogs/ -mv components/delete-all-archived-sessions-dialog.tsx → agent-view/dialogs/ -``` - ---- - -## Web + Electron Dual Support - -### Platform Detection - -```typescript -// src/lib/platform.ts -export const isElectron = typeof window !== 'undefined' && 'electronAPI' in window; - -export const platform = { - isElectron, - isWeb: !isElectron, - isMac: isElectron ? window.electronAPI.platform === 'darwin' : false, - isWindows: isElectron ? window.electronAPI.platform === 'win32' : false, - isLinux: isElectron ? window.electronAPI.platform === 'linux' : false, -}; -``` - -### API Abstraction Layer - -```typescript -// src/lib/api/file-picker.ts -import { platform } from '../platform'; - -export interface FilePickerResult { - canceled: boolean; - paths: string[]; -} - -export async function pickDirectory(): Promise { - if (platform.isElectron) { - const result = await window.dialogAPI.openDirectory(); - return { canceled: result.canceled, paths: result.filePaths || [] }; - } - - // Web fallback using File System Access API - try { - const handle = await window.showDirectoryPicker(); - return { canceled: false, paths: [handle.name] }; - } catch (error) { - if ((error as Error).name === 'AbortError') { - return { canceled: true, paths: [] }; - } - throw error; - } -} - -export async function pickFile(options?: { - accept?: Record; -}): Promise { - if (platform.isElectron) { - const result = await window.dialogAPI.openFile({ - filters: options?.accept - ? Object.entries(options.accept).map(([name, extensions]) => ({ - name, - extensions, - })) - : undefined, - }); - return { canceled: result.canceled, paths: result.filePaths || [] }; - } - - // Web fallback - try { - const [handle] = await window.showOpenFilePicker({ - types: options?.accept - ? Object.entries(options.accept).map(([description, accept]) => ({ - description, - accept: { 'application/*': accept }, - })) - : undefined, - }); - return { canceled: false, paths: [handle.name] }; - } catch (error) { - if ((error as Error).name === 'AbortError') { - return { canceled: true, paths: [] }; - } - throw error; - } -} -``` - ---- - -## Migration Phases - -### Phase 1: Foundation (Week 1-2) - -**Goal**: Set up new build infrastructure without breaking existing functionality. - -- [x] Create `vite.config.mts` with electron plugins -- [x] Create `electron/tsconfig.json` for Electron TypeScript -- [x] Convert `electron/main.js` → `electron/main.ts` -- [x] Convert `electron/preload.js` → `electron/preload.ts` -- [ ] Implement IPC schema and type-safe handlers (deferred - using existing HTTP API) -- [x] Set up TanStack Router configuration -- [ ] Port debug console from starter template (deferred) -- [x] Create `index.html` for Vite entry - -**Deliverables**: - -- [x] Working Vite dev server -- [x] TypeScript Electron main process -- [ ] Debug console functional (deferred) - -### Phase 2: Core Migration (Week 3-4) - -**Goal**: Replace Next.js with Vite while maintaining feature parity. - -- [x] Create `src/renderer.tsx` entry point -- [x] Create `src/App.tsx` root component -- [x] Set up TanStack Router with file-based routes -- [x] Port all views to route files -- [x] Update environment variables (`NEXT_PUBLIC_*` → `VITE_*`) -- [x] Verify Zustand stores work unchanged -- [x] Verify HTTP API client works unchanged -- [x] Test Electron build -- [ ] Test Web build (needs verification) - -**Additional completed tasks**: - -- [x] Remove all "use client" directives (not needed in Vite) -- [x] Replace all `setCurrentView()` calls with TanStack Router `navigate()` -- [x] Rename `apps/app` to `apps/ui` -- [x] Update package.json scripts -- [x] Configure memory history for Electron (no URL bar) -- [x] Fix ES module imports (replace `require()` with `import`) -- [x] Remove PostCSS config (using `@tailwindcss/vite` plugin) - -**Deliverables**: - -- [x] All views accessible via TanStack Router -- [x] Electron build functional -- [ ] Web build functional (needs testing) -- [x] No regression in existing functionality - -### Phase 3: Component Refactoring (Week 5-7) - -**Goal**: Refactor large view files to follow folder-pattern.md. - -- [ ] Refactor `spec-view.tsx` (1,230 lines) -- [ ] Refactor `analysis-view.tsx` (1,134 lines) -- [ ] Refactor `agent-view.tsx` (916 lines) -- [ ] Refactor `welcome-view.tsx` (815 lines) -- [ ] Refactor `context-view.tsx` (735 lines) -- [ ] Refactor `terminal-view.tsx` (697 lines) -- [ ] Refactor `interview-view.tsx` (637 lines) -- [ ] Reorganize `settings-view` dialogs - -**Deliverables**: - -- All views under 500 lines -- Consistent folder structure across all views -- Barrel exports for all component folders - -### Phase 4: Package Extraction (Week 8) - -**Goal**: Create shared packages for better modularity. - -- [ ] Create `libs/types/` package -- [ ] Create `libs/utils/` package -- [ ] Create `libs/platform/` package -- [ ] Create `libs/model-resolver/` package -- [ ] Create `libs/ipc-types/` package -- [ ] Update imports across apps - -**Deliverables**: - -- 5 new shared packages -- No code duplication between apps -- Clean dependency graph - -### Phase 5: Polish & Testing (Week 9-10) - -**Goal**: Ensure production readiness. - -- [ ] Write E2E tests with Playwright -- [ ] Performance benchmarking -- [ ] Bundle size optimization -- [ ] Documentation updates -- [ ] CI/CD pipeline updates -- [ ] Remove Next.js dependencies - -**Deliverables**: - -- Comprehensive test coverage -- Performance metrics documentation -- Updated CI/CD configuration -- Clean package.json (no Next.js) - ---- - -## Expected Benefits - -### Developer Experience - -| Aspect | Before | After | -| ---------------------- | ------------- | ------------------ | -| Dev server startup | 8-15 seconds | 1-3 seconds | -| Hot Module Replacement | 500ms-2s | 50-100ms | -| TypeScript in Electron | Not supported | Full support | -| Debug tooling | Limited | Full debug console | -| Build times | 45-90 seconds | 15-30 seconds | - -### Code Quality - -| Aspect | Before | After | -| ---------------------- | ------------ | --------------------- | -| Electron type safety | 0% | 100% | -| Component organization | Inconsistent | Standardized | -| Code sharing | None | 5 shared packages | -| Path handling | Ad-hoc | Centralized utilities | - -### Bundle Size - -| Aspect | Before | After | -| ------------------ | ------- | ------- | -| Next.js runtime | ~200KB | 0KB | -| Framework overhead | High | Minimal | -| Tree shaking | Limited | Full | - ---- - -## Risk Mitigation - -### Rollback Strategy - -1. **Branch-based development**: All work on feature branch -2. **Parallel running**: Keep Next.js functional until migration complete -3. **Feature flags**: Toggle between old/new implementations -4. **Comprehensive testing**: E2E tests before/after comparison - -### Known Challenges - -| Challenge | Mitigation | -| --------------------- | ------------------------------------------------ | -| Route migration | TanStack Router has similar file-based routing | -| Environment variables | Simple search/replace (`NEXT_PUBLIC_` → `VITE_`) | -| Build configuration | Reference electron-starter-template | -| SSR considerations | N/A - we don't use SSR | - -### Testing Strategy - -1. **Unit tests**: Vitest for component/utility testing -2. **Integration tests**: Test IPC communication -3. **E2E tests**: Playwright for full application testing -4. **Manual testing**: QA checklist for each view - ---- - -## Appendix: Vite Configuration Reference - -```typescript -// vite.config.mts -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import electron from 'vite-plugin-electron'; -import renderer from 'vite-plugin-electron-renderer'; -import { TanStackRouterVite } from '@tanstack/router-plugin/vite'; -import tailwindcss from '@tailwindcss/vite'; -import path from 'path'; - -export default defineConfig({ - plugins: [ - react({ - babel: { - plugins: [['babel-plugin-react-compiler', {}]], - }, - }), - TanStackRouterVite({ - routesDirectory: './src/routes', - generatedRouteTree: './src/routeTree.gen.ts', - autoCodeSplitting: true, - }), - tailwindcss(), - electron([ - { - entry: 'electron/main.ts', - vite: { - build: { - outDir: 'dist-electron', - rollupOptions: { - external: ['electron'], - }, - }, - }, - }, - { - entry: 'electron/preload.ts', - onstart: ({ reload }) => reload(), - vite: { - build: { - outDir: 'dist-electron', - rollupOptions: { - external: ['electron'], - }, - }, - }, - }, - ]), - renderer(), - ], - resolve: { - alias: { - '@': path.resolve(__dirname, 'src'), - '@electron': path.resolve(__dirname, 'electron'), - }, - }, - build: { - outDir: 'dist', - }, -}); -``` - ---- - -## Document History - -| Version | Date | Author | Changes | -| ------- | -------- | ------ | ------------------------------------------------------------------------------------- | -| 1.0 | Dec 2025 | Team | Initial migration plan | -| 1.1 | Dec 2025 | Team | Phase 1 & 2 complete. Updated checkboxes, added completed tasks, noted deferred items | - ---- - -**Next Steps**: - -1. ~~Review and approve this plan~~ ✅ -2. ~~Wait for `feature/worktrees` branch merge~~ ✅ -3. ~~Create migration branch~~ ✅ (refactor/frontend) -4. ~~Complete Phase 1 implementation~~ ✅ -5. ~~Complete Phase 2 implementation~~ ✅ -6. **Current**: Verify web build works, then begin Phase 3 (Component Refactoring) -7. Consider implementing deferred items: Debug console, IPC schema diff --git a/docs/pipeline-feature.md b/docs/pipeline-feature.md deleted file mode 100644 index 59238776..00000000 --- a/docs/pipeline-feature.md +++ /dev/null @@ -1,156 +0,0 @@ -# Pipeline Feature - -Custom pipeline steps that run automatically after a feature completes "In Progress", creating a sequential workflow for code review, security audits, testing, and more. - -## Overview - -The pipeline feature allows users to define custom workflow steps that execute automatically after the main implementation phase. Each step prompts the agent with specific instructions while maintaining the full conversation context. - -## How It Works - -1. **Feature completes "In Progress"** - When the agent finishes implementing a feature -2. **Pipeline steps execute sequentially** - Each configured step runs in order -3. **Agent receives instructions** - The step's instructions are sent to the agent -4. **Context preserved** - Full chat history is maintained between steps -5. **Final status** - After all steps complete, the feature moves to "Waiting Approval" or "Verified" - -## Configuration - -### Accessing Pipeline Settings - -- Click the **gear icon** on the "In Progress" column header -- Or click the gear icon on any pipeline step column - -### Adding Pipeline Steps - -1. Click **"Add Pipeline Step"** -2. Optionally select a **pre-built template** from the dropdown: - - Code Review - - Security Review - - Testing - - Documentation - - Performance Optimization -3. Customize the **Step Name** -4. Choose a **Color** for the column -5. Write or modify the **Agent Instructions** -6. Click **"Add Step"** - -### Managing Steps - -- **Reorder**: Use the up/down arrows to change step order -- **Edit**: Click the pencil icon to modify a step -- **Delete**: Click the trash icon to remove a step -- **Load from file**: Upload a `.md` or `.txt` file for instructions - -## Storage - -Pipeline configuration is stored per-project at: - -``` -{project}/.automaker/pipeline.json -``` - -## Pre-built Templates - -### Code Review - -Comprehensive code quality review covering: - -- Readability and maintainability -- DRY principle and single responsibility -- Best practices and conventions -- Performance considerations -- Test coverage - -### Security Review - -OWASP-focused security audit including: - -- Input validation and sanitization -- SQL injection and XSS prevention -- Authentication and authorization -- Data protection -- Common vulnerability checks (OWASP Top 10) - -### Testing - -Test coverage verification: - -- Unit test requirements -- Integration testing -- Test quality standards -- Running and validating tests - -### Documentation - -Documentation requirements: - -- Code documentation (JSDoc/docstrings) -- API documentation -- README updates -- Changelog entries - -### Performance Optimization - -Performance review covering: - -- Algorithm optimization -- Memory usage -- Database/API optimization -- Frontend performance (if applicable) - -## UI Changes - -### Kanban Board - -- Pipeline columns appear between "In Progress" and "Waiting Approval" -- Each pipeline column shows features currently in that step -- Gear icon on columns opens pipeline settings - -### Horizontal Scrolling - -- Board supports horizontal scrolling when many columns exist -- Minimum window width reduced to 600px to accommodate various screen sizes - -## Technical Details - -### Files Modified - -**Types:** - -- `libs/types/src/pipeline.ts` - PipelineStep, PipelineConfig types -- `libs/types/src/index.ts` - Export pipeline types - -**Server:** - -- `apps/server/src/services/pipeline-service.ts` - CRUD operations, status transitions -- `apps/server/src/routes/pipeline/` - API endpoints -- `apps/server/src/services/auto-mode-service.ts` - Pipeline execution integration - -**UI:** - -- `apps/ui/src/store/app-store.ts` - Pipeline state management -- `apps/ui/src/lib/http-api-client.ts` - Pipeline API client -- `apps/ui/src/components/views/board-view/constants.ts` - Dynamic column generation -- `apps/ui/src/components/views/board-view/kanban-board.tsx` - Pipeline props, scrolling -- `apps/ui/src/components/views/board-view/dialogs/pipeline-settings-dialog.tsx` - Settings UI -- `apps/ui/src/hooks/use-responsive-kanban.ts` - Scroll support - -### API Endpoints - -``` -POST /api/pipeline/config - Get pipeline config -POST /api/pipeline/config/save - Save pipeline config -POST /api/pipeline/steps/add - Add a step -POST /api/pipeline/steps/update - Update a step -POST /api/pipeline/steps/delete - Delete a step -POST /api/pipeline/steps/reorder - Reorder steps -``` - -### Status Flow - -``` -backlog → in_progress → pipeline_step1 → pipeline_step2 → ... → verified/waiting_approval -``` - -Pipeline statuses use the format `pipeline_{stepId}` to support unlimited dynamic steps. diff --git a/docs/plans/2025-12-29-api-security-hardening-design.md b/docs/plans/2025-12-29-api-security-hardening-design.md deleted file mode 100644 index 54c0ca67..00000000 --- a/docs/plans/2025-12-29-api-security-hardening-design.md +++ /dev/null @@ -1,94 +0,0 @@ -# API Security Hardening Design - -**Date:** 2025-12-29 -**Branch:** protect-api-with-api-key -**Status:** Approved - -## Overview - -Security improvements for the API authentication system before merging the PR. These changes harden the existing implementation for production deployment scenarios (local, Docker, internet-exposed). - -## Fixes to Implement - -### 1. Use Short-Lived wsToken for WebSocket Authentication - -**Problem:** The client currently passes `sessionToken` in WebSocket URL query parameters. Query params get logged and can leak credentials. - -**Solution:** Update the client to: - -1. Fetch a wsToken from `/api/auth/token` before each WebSocket connection -2. Use `wsToken` query param instead of `sessionToken` -3. Never put session tokens in URLs - -**Files to modify:** - -- `apps/ui/src/lib/http-api-client.ts` - Update `connectWebSocket()` to fetch wsToken first - ---- - -### 2. Add Environment Variable to Hide API Key from Logs - -**Problem:** The API key is printed to console on startup, which gets captured by logging systems in production. - -**Solution:** Add `AUTOMAKER_HIDE_API_KEY=true` env var to suppress the banner. - -**Files to modify:** - -- `apps/server/src/lib/auth.ts` - Wrap console.log banner in env var check - ---- - -### 3. Add Rate Limiting to Login Endpoint - -**Problem:** No brute force protection on `/api/auth/login`. Attackers could attempt many API keys. - -**Solution:** Add basic in-memory rate limiting: - -- ~5 attempts per minute per IP -- In-memory Map tracking (resets on server restart) -- Return 429 Too Many Requests when exceeded - -**Files to modify:** - -- `apps/server/src/routes/auth/index.ts` - Add rate limiting logic to login handler - ---- - -### 4. Use Timing-Safe Comparison for API Key - -**Problem:** Using `===` for API key comparison is vulnerable to timing attacks. - -**Solution:** Use `crypto.timingSafeEqual()` for constant-time comparison. - -**Files to modify:** - -- `apps/server/src/lib/auth.ts` - Update `validateApiKey()` function - ---- - -### 5. Make WebSocket Tokens Single-Use - -**Problem:** wsTokens can be reused within the 5-minute window. If intercepted, attackers have time to use them. - -**Solution:** Delete the token after first successful validation. - -**Files to modify:** - -- `apps/server/src/lib/auth.ts` - Update `validateWsConnectionToken()` to delete after use - ---- - -## Implementation Order - -1. Fix #4 (timing-safe comparison) - Simple, isolated change -2. Fix #5 (single-use wsToken) - Simple, isolated change -3. Fix #2 (hide API key env var) - Simple, isolated change -4. Fix #3 (rate limiting) - Moderate complexity -5. Fix #1 (client wsToken usage) - Requires coordination with server - -## Testing Notes - -- Test login with rate limiting (verify 429 after 5 attempts) -- Test WebSocket connection with new wsToken flow -- Test wsToken is invalidated after first use -- Verify `AUTOMAKER_HIDE_API_KEY=true` suppresses banner diff --git a/package-lock.json b/package-lock.json index d8190a03..075e5fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -455,6 +455,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1038,6 +1039,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz", "integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1080,6 +1082,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1900,7 +1903,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1922,7 +1924,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1939,7 +1940,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1954,7 +1954,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -2722,7 +2721,6 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=18" } @@ -2847,7 +2845,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2864,7 +2861,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2881,7 +2877,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -2990,7 +2985,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3013,7 +3007,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3036,7 +3029,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3122,7 +3114,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.7.0" }, @@ -3145,7 +3136,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3165,7 +3155,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -3565,8 +3554,7 @@ "version": "16.0.10", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "16.0.10", @@ -3580,7 +3568,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3597,7 +3584,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3614,7 +3600,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3631,7 +3616,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3648,7 +3632,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3665,7 +3648,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3682,7 +3664,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3699,7 +3680,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -3790,6 +3770,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -5230,7 +5211,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -5564,6 +5544,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.141.6.tgz", "integrity": "sha512-qWFxi2D6eGc1L03RzUuhyEOplZ7Q6q62YOl7Of9Y0q4YjwQwxRm4zxwDVtvUIoy4RLVCpqp5UoE+Nxv2PY9trg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", @@ -5990,6 +5971,7 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -6132,6 +6114,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6142,6 +6125,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6247,6 +6231,7 @@ "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", @@ -6740,7 +6725,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.10.0", @@ -6838,6 +6824,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6898,6 +6885,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7496,6 +7484,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8027,8 +8016,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -8333,8 +8321,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-env": { "version": "10.1.0", @@ -8431,6 +8418,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8732,6 +8720,7 @@ "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", @@ -9058,7 +9047,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -9079,7 +9067,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -9330,6 +9317,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9644,6 +9632,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11311,7 +11300,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -11373,7 +11361,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -13801,7 +13788,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -13818,7 +13804,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -13836,7 +13821,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -13953,9 +13937,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14025,6 +14009,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14034,6 +14019,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14392,7 +14378,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14581,6 +14566,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.0.tgz", "integrity": "sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -14629,7 +14615,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14680,7 +14665,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14703,7 +14687,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14726,7 +14709,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14743,7 +14725,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14760,7 +14741,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14777,7 +14757,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14794,7 +14773,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14811,7 +14789,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14828,7 +14805,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -14845,7 +14821,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14868,7 +14843,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14891,7 +14865,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14914,7 +14887,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14937,7 +14909,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -14960,7 +14931,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -15429,7 +15399,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -15599,7 +15568,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -15663,7 +15631,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -15761,6 +15728,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15965,6 +15933,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16336,6 +16305,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -16425,7 +16395,8 @@ "resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz", "integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/vite/node_modules/fdir": { "version": "6.5.0", @@ -16451,6 +16422,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16493,6 +16465,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -16750,6 +16723,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -16818,6 +16792,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }