Files
automaker/docs/migration-plan-nextjs-to-vite.md
SuperComboGamer 8d578558ff style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:31:57 -05:00

1830 lines
59 KiB
Markdown

# 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<string> {
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<void> {
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<boolean> {
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<fs.Stats | null> {
try {
return await fs.promises.stat(filePath);
} catch {
return null;
}
}
/**
* Recursively removes a directory
*/
export async function removeDirectory(dirPath: string): Promise<void> {
await fs.promises.rm(dirPath, { recursive: true, force: true });
}
/**
* Copies a file or directory
*/
export async function copy(src: string, dest: string): Promise<void> {
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<T = unknown> {
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<OpenDialogOptions>;
response: DialogResult<string[]>;
};
'dialog:openFile': {
request: Partial<OpenDialogOptions>;
response: DialogResult<string[]>;
};
'dialog:saveFile': {
request: Partial<SaveDialogOptions>;
response: DialogResult<string>;
};
// 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<T extends IPCChannel> = IPCSchema[T]['request'];
export type IPCResponse<T extends IPCChannel> = 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<FilePickerResult> {
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<string, string[]>;
}): Promise<FilePickerResult> {
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