Implement Electron app auto-updater (Phase 1 & 2)
Phase 1: Update Notifications & Download - Install electron-updater dependency - Add GitHub publish configuration to package.json Phase 2: Auto-Download & Install - Backend: Main process auto-updater with electron-updater - Frontend: AppUpdateNotification UI component - IPC: Full update workflow (check/download/install/events) Backend Implementation: - Created app-updater.ts with autoUpdater configuration - Auto-download enabled, checks every 4 hours - Events: update-available, update-downloaded, download-progress - Created app-update-handlers.ts with IPC handlers - Integrated into main/index.ts (production only) Frontend Implementation: - Created AppUpdateNotification.tsx with modal dialog - Shows version, release notes, download progress - Download workflow: Available → Downloading → Downloaded → Install - Created app-update-api.ts preload bindings - Integrated into App.tsx root level Configuration: - Added 8 IPC channels: APP_UPDATE_* operations & events - Created AppUpdateInfo, AppUpdateProgress, AppUpdate*Event types - GitHub release publishing: provider=github, repo=Auto-Claude Features: ✅ Automatic update checking (3s after launch, every 4 hours) ✅ Auto-download updates in background with progress bar ✅ User notification with version & release notes ✅ One-click install and restart ✅ Dismissible with "Remind Me Later" ✅ Error handling & visual feedback ✅ Production-only (disabled in dev mode) Non-technical users can now update the Electron app with one click! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
# Auto Claude Update System Analysis
|
||||
|
||||
## Current State
|
||||
|
||||
The app has **TWO separate update systems** for different components:
|
||||
|
||||
### 1. ✅ Auto Claude Framework Updates (WORKING)
|
||||
|
||||
**What it updates:** The Python framework source code (`auto-claude/` directory)
|
||||
|
||||
**How it works:**
|
||||
- Checks GitHub Releases API for new versions
|
||||
- Downloads release tarball
|
||||
- Extracts and applies update to the bundled source
|
||||
- Preserves user configuration files (.env, etc.)
|
||||
|
||||
**User Experience:**
|
||||
- **Settings > Advanced > Updates** section
|
||||
- Visual update checker with version display
|
||||
- Release notes rendered in UI
|
||||
- Progress bar during download
|
||||
- One-click update button
|
||||
- Works across all platforms (macOS, Windows, Linux)
|
||||
|
||||
**Files:**
|
||||
- `auto-claude-ui/src/main/auto-claude-updater.ts` - Main updater module
|
||||
- `auto-claude-ui/src/main/updater/update-checker.ts` - Update checking
|
||||
- `auto-claude-ui/src/main/updater/update-installer.ts` - Download & install
|
||||
- `auto-claude-ui/src/main/ipc-handlers/autobuild-source-handlers.ts` - IPC handlers
|
||||
- `auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx` - UI
|
||||
|
||||
**Status:** ✅ **FULLY FUNCTIONAL** - Non-technical users can update the framework with one click!
|
||||
|
||||
---
|
||||
|
||||
### 2. ❌ Electron App Updates (NOT IMPLEMENTED)
|
||||
|
||||
**What it updates:** The Electron application itself (Auto Claude UI)
|
||||
|
||||
**Current State:**
|
||||
- ❌ No `electron-updater` dependency installed
|
||||
- ❌ No auto-update configuration in electron-builder
|
||||
- ❌ No update checking in main process
|
||||
- ❌ No UI for app update notifications
|
||||
- ❌ Users must manually download new releases from GitHub
|
||||
|
||||
**What users currently need to do:**
|
||||
1. Go to GitHub Releases page
|
||||
2. Download the appropriate installer (.dmg, .exe, .AppImage, etc.)
|
||||
3. Run the installer
|
||||
4. Manually replace the old app
|
||||
|
||||
---
|
||||
|
||||
## What Needs to Be Implemented
|
||||
|
||||
To enable automatic Electron app updates for non-technical users, we need to add:
|
||||
|
||||
### 1. Install electron-updater
|
||||
|
||||
```bash
|
||||
npm install electron-updater
|
||||
```
|
||||
|
||||
### 2. Configure electron-builder for Publishing
|
||||
|
||||
Add to `package.json` build config:
|
||||
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "AndyMik90",
|
||||
"repo": "Auto-Claude"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Auto-Update Logic in Main Process
|
||||
|
||||
Create `auto-claude-ui/src/main/app-updater.ts`:
|
||||
|
||||
```typescript
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
|
||||
export function initializeAppUpdater(mainWindow: BrowserWindow) {
|
||||
// Configure update checking
|
||||
autoUpdater.autoDownload = false; // Let user decide
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
// Check for updates on launch (after 3 seconds)
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}, 3000);
|
||||
|
||||
// Check periodically (every 4 hours)
|
||||
setInterval(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}, 4 * 60 * 60 * 1000);
|
||||
|
||||
// Event handlers
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
mainWindow.webContents.send('app-update-available', {
|
||||
version: info.version,
|
||||
releaseNotes: info.releaseNotes,
|
||||
releaseDate: info.releaseDate
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
mainWindow.webContents.send('app-update-downloaded', {
|
||||
version: info.version
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
console.error('App update error:', error);
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
mainWindow.webContents.send('app-update-progress', {
|
||||
percent: progress.percent,
|
||||
transferred: progress.transferred,
|
||||
total: progress.total
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// IPC handlers
|
||||
export function registerAppUpdateHandlers() {
|
||||
ipcMain.handle('app-update-download', async () => {
|
||||
await autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
ipcMain.handle('app-update-install', () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
ipcMain.handle('app-update-check', async () => {
|
||||
return await autoUpdater.checkForUpdates();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add UI Notification Component
|
||||
|
||||
Create an update banner or modal in the renderer that:
|
||||
- Shows when app update is available
|
||||
- Displays version and release notes
|
||||
- Has "Download Update" button
|
||||
- Shows download progress
|
||||
- Has "Install and Restart" button after download
|
||||
|
||||
### 5. Update GitHub Release Workflow
|
||||
|
||||
Ensure GitHub releases are created with proper assets:
|
||||
- macOS: `.dmg` and `.zip` files + `latest-mac.yml`
|
||||
- Windows: `.exe` installer + `latest.yml`
|
||||
- Linux: `.AppImage` and `.deb` + `latest-linux.yml`
|
||||
|
||||
The `latest-*.yml` files are auto-generated by electron-builder and contain update metadata.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Option A: Full Auto-Update (Recommended)
|
||||
|
||||
**Pros:**
|
||||
- Best user experience
|
||||
- Automatic background downloads
|
||||
- One-click install
|
||||
- Industry standard
|
||||
|
||||
**Cons:**
|
||||
- Requires code signing certificates for production (macOS, Windows)
|
||||
- Without signing, users get security warnings
|
||||
|
||||
### Option B: Update Notification Only
|
||||
|
||||
**Pros:**
|
||||
- Simpler implementation
|
||||
- No code signing required
|
||||
- User downloads from GitHub (trusted source)
|
||||
|
||||
**Cons:**
|
||||
- Users still need to manually download and install
|
||||
- Less convenient than auto-update
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Just notify, don't auto-download
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
// Show notification with link to GitHub Releases
|
||||
mainWindow.webContents.send('app-update-available', {
|
||||
version: info.version,
|
||||
downloadUrl: `https://github.com/AndyMik90/Auto-Claude/releases/tag/v${info.version}`
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development vs Production
|
||||
|
||||
**Important:** `electron-updater` only works in **packaged apps**, not in development mode.
|
||||
|
||||
During development:
|
||||
```typescript
|
||||
if (app.isPackaged) {
|
||||
initializeAppUpdater(mainWindow);
|
||||
} else {
|
||||
console.log('[Dev] Auto-updater disabled in development mode');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Signing Requirements
|
||||
|
||||
For production auto-updates without security warnings:
|
||||
|
||||
### macOS
|
||||
- Requires Apple Developer account ($99/year)
|
||||
- Code signing certificate
|
||||
- Notarization with Apple
|
||||
|
||||
### Windows
|
||||
- Requires code signing certificate (~$200-400/year)
|
||||
- Without: Windows SmartScreen warnings
|
||||
|
||||
### Linux
|
||||
- No code signing required
|
||||
- Users may need to mark `.AppImage` as executable
|
||||
|
||||
---
|
||||
|
||||
## Testing Auto-Updates
|
||||
|
||||
1. **Local Testing:**
|
||||
- Build and package: `npm run package`
|
||||
- Create local update server or use GitHub Releases
|
||||
- Test with different versions
|
||||
|
||||
2. **GitHub Releases Testing:**
|
||||
- Create a draft release on GitHub
|
||||
- Publish with version tag (e.g., `v2.4.0`)
|
||||
- electron-builder automatically uploads assets
|
||||
- Test with previous version installed
|
||||
|
||||
---
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
1. **Phase 1: Add Update Notification** (Quick win)
|
||||
- Install `electron-updater`
|
||||
- Add basic update checking
|
||||
- Show notification with link to GitHub Releases
|
||||
- No auto-download, users download manually
|
||||
|
||||
2. **Phase 2: Enable Auto-Download** (Better UX)
|
||||
- Add download progress UI
|
||||
- Enable auto-download of updates
|
||||
- Add "Install and Restart" button
|
||||
|
||||
3. **Phase 3: Code Signing** (Production ready)
|
||||
- Acquire code signing certificates
|
||||
- Configure signing in electron-builder
|
||||
- Notarize macOS builds
|
||||
- Sign Windows builds
|
||||
|
||||
---
|
||||
|
||||
## Current Framework Update Flow (Already Working!)
|
||||
|
||||
For reference, here's how the existing Auto Claude framework updater works:
|
||||
|
||||
1. User opens **Settings > Advanced > Updates**
|
||||
2. App checks GitHub Releases API
|
||||
3. If update available, shows version + release notes
|
||||
4. User clicks "Download Update"
|
||||
5. Progress bar shows download status
|
||||
6. Update is extracted and applied to bundled source
|
||||
7. User configuration (.env) is preserved
|
||||
8. Done - no restart needed!
|
||||
|
||||
**This same UX could be replicated for app updates!**
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Component | Status | User Experience |
|
||||
|-----------|--------|-----------------|
|
||||
| Auto Claude Framework | ✅ Working | One-click update in Settings |
|
||||
| Electron App (UI) | ❌ Missing | Must download from GitHub manually |
|
||||
|
||||
**Recommendation:** Implement electron-updater with update notifications (Phase 1) as a quick win. This will enable non-technical users to update the app without using git or terminal commands.
|
||||
|
||||
**Estimated effort:**
|
||||
- Phase 1 (Notifications): 2-4 hours
|
||||
- Phase 2 (Auto-download): 2-3 hours
|
||||
- Phase 3 (Code signing): Varies by platform
|
||||
|
||||
The existing framework updater code provides an excellent reference for the UI implementation!
|
||||
Generated
+1541
-21
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@
|
||||
"chokidar": "^5.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"electron-updater": "^6.6.2",
|
||||
"ioredis": "^5.8.2",
|
||||
"lucide-react": "^0.560.0",
|
||||
"motion": "^12.23.26",
|
||||
@@ -114,6 +115,13 @@
|
||||
"build": {
|
||||
"appId": "com.autoclaude.ui",
|
||||
"productName": "Auto Claude",
|
||||
"publish": [
|
||||
{
|
||||
"provider": "github",
|
||||
"owner": "AndyMik90",
|
||||
"repo": "Auto-Claude"
|
||||
}
|
||||
],
|
||||
"directories": {
|
||||
"output": "dist",
|
||||
"buildResources": "resources"
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Electron App Auto-Updater
|
||||
*
|
||||
* Manages automatic updates for the packaged Electron application using electron-updater.
|
||||
* Updates are published through GitHub Releases and automatically downloaded and installed.
|
||||
*
|
||||
* Update flow:
|
||||
* 1. Check for updates 3 seconds after app launch
|
||||
* 2. Download updates automatically when available
|
||||
* 3. Notify user when update is downloaded
|
||||
* 4. Install and restart when user confirms
|
||||
*
|
||||
* Events sent to renderer:
|
||||
* - APP_UPDATE_AVAILABLE: New update available (with version info)
|
||||
* - APP_UPDATE_DOWNLOADED: Update downloaded and ready to install
|
||||
* - APP_UPDATE_PROGRESS: Download progress updates
|
||||
* - APP_UPDATE_ERROR: Error during update process
|
||||
*/
|
||||
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import { IPC_CHANNELS } from '../shared/constants';
|
||||
import type { AppUpdateInfo } from '../shared/types';
|
||||
|
||||
// Configure electron-updater
|
||||
autoUpdater.autoDownload = true; // Automatically download updates when available
|
||||
autoUpdater.autoInstallOnAppQuit = true; // Automatically install on app quit
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the app updater system
|
||||
*
|
||||
* Sets up event handlers and starts periodic update checks.
|
||||
* Should only be called in production (app.isPackaged).
|
||||
*
|
||||
* @param window - The main BrowserWindow for sending update events
|
||||
*/
|
||||
export function initializeAppUpdater(window: BrowserWindow): void {
|
||||
mainWindow = window;
|
||||
|
||||
// Log updater configuration
|
||||
console.log('[app-updater] Initializing app auto-updater');
|
||||
console.log('[app-updater] Current version:', autoUpdater.currentVersion.version);
|
||||
console.log('[app-updater] Auto-download enabled:', autoUpdater.autoDownload);
|
||||
|
||||
// ============================================
|
||||
// Event Handlers
|
||||
// ============================================
|
||||
|
||||
// Update available - new version found
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
console.log('[app-updater] Update available:', info.version);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_AVAILABLE, {
|
||||
version: info.version,
|
||||
releaseNotes: info.releaseNotes,
|
||||
releaseDate: info.releaseDate
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update downloaded - ready to install
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('[app-updater] Update downloaded:', info.version);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, {
|
||||
version: info.version,
|
||||
releaseNotes: info.releaseNotes,
|
||||
releaseDate: info.releaseDate
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Download progress
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
console.log(`[app-updater] Download progress: ${progress.percent.toFixed(2)}%`);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_PROGRESS, {
|
||||
percent: progress.percent,
|
||||
bytesPerSecond: progress.bytesPerSecond,
|
||||
transferred: progress.transferred,
|
||||
total: progress.total
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
autoUpdater.on('error', (error) => {
|
||||
console.error('[app-updater] Update error:', error);
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_ERROR, {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// No update available
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
console.log('[app-updater] No updates available. Current version:', info.version);
|
||||
});
|
||||
|
||||
// Checking for updates
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.log('[app-updater] Checking for updates...');
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Update Check Schedule
|
||||
// ============================================
|
||||
|
||||
// Check for updates 3 seconds after launch
|
||||
setTimeout(() => {
|
||||
console.log('[app-updater] Performing initial update check');
|
||||
autoUpdater.checkForUpdates().catch((error) => {
|
||||
console.error('[app-updater] Initial update check failed:', error);
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
// Check for updates every 4 hours
|
||||
const FOUR_HOURS = 4 * 60 * 60 * 1000;
|
||||
setInterval(() => {
|
||||
console.log('[app-updater] Performing periodic update check');
|
||||
autoUpdater.checkForUpdates().catch((error) => {
|
||||
console.error('[app-updater] Periodic update check failed:', error);
|
||||
});
|
||||
}, FOUR_HOURS);
|
||||
|
||||
console.log('[app-updater] Auto-updater initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually check for updates
|
||||
* Called from IPC handler when user requests manual check
|
||||
*/
|
||||
export async function checkForUpdates(): Promise<AppUpdateInfo | null> {
|
||||
try {
|
||||
console.log('[app-updater] Manual update check requested');
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updateAvailable = result.updateInfo.version !== autoUpdater.currentVersion.version;
|
||||
|
||||
if (!updateAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
version: result.updateInfo.version,
|
||||
releaseNotes: result.updateInfo.releaseNotes as string | undefined,
|
||||
releaseDate: result.updateInfo.releaseDate
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[app-updater] Manual update check failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually download update
|
||||
* Called from IPC handler when user requests manual download
|
||||
*/
|
||||
export async function downloadUpdate(): Promise<void> {
|
||||
try {
|
||||
console.log('[app-updater] Manual update download requested');
|
||||
await autoUpdater.downloadUpdate();
|
||||
} catch (error) {
|
||||
console.error('[app-updater] Manual update download failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit and install update
|
||||
* Called from IPC handler when user confirms installation
|
||||
*/
|
||||
export function quitAndInstall(): void {
|
||||
console.log('[app-updater] Quitting and installing update');
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current app version
|
||||
*/
|
||||
export function getCurrentVersion(): string {
|
||||
return autoUpdater.currentVersion.version;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { TerminalManager } from './terminal-manager';
|
||||
import { pythonEnvManager } from './python-env-manager';
|
||||
import { getUsageMonitor } from './claude-profile/usage-monitor';
|
||||
import { initializeUsageMonitorForwarding } from './ipc-handlers/terminal-handlers';
|
||||
import { initializeAppUpdater } from './app-updater';
|
||||
|
||||
// Get icon path based on platform
|
||||
function getIconPath(): string {
|
||||
@@ -137,6 +138,14 @@ app.whenReady().then(() => {
|
||||
const usageMonitor = getUsageMonitor();
|
||||
usageMonitor.start();
|
||||
console.log('[main] Usage monitor initialized and started');
|
||||
|
||||
// Initialize app auto-updater (only in production)
|
||||
if (app.isPackaged) {
|
||||
initializeAppUpdater(mainWindow);
|
||||
console.log('[main] App auto-updater initialized');
|
||||
} else {
|
||||
console.log('[main] App auto-updater disabled in development mode');
|
||||
}
|
||||
}
|
||||
|
||||
// macOS: re-create window when dock icon is clicked
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* App Update IPC Handlers
|
||||
*
|
||||
* Handles IPC communication for Electron app auto-updates.
|
||||
* Provides manual controls for checking, downloading, and installing updates.
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../shared/constants';
|
||||
import type { IPCResult, AppUpdateInfo } from '../../shared/types';
|
||||
import {
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
quitAndInstall,
|
||||
getCurrentVersion
|
||||
} from '../app-updater';
|
||||
|
||||
/**
|
||||
* Register all app-update-related IPC handlers
|
||||
*/
|
||||
export function registerAppUpdateHandlers(): void {
|
||||
console.log('[IPC] Registering app update handlers');
|
||||
|
||||
// ============================================
|
||||
// App Update Operations
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* APP_UPDATE_CHECK: Manually check for updates
|
||||
* Returns update availability and version information
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.APP_UPDATE_CHECK,
|
||||
async (): Promise<IPCResult<AppUpdateInfo | null>> => {
|
||||
try {
|
||||
const result = await checkForUpdates();
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
console.error('[app-update-handlers] Check for updates failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to check for updates'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* APP_UPDATE_DOWNLOAD: Manually download update
|
||||
* Triggers download of available update
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.APP_UPDATE_DOWNLOAD,
|
||||
async (): Promise<IPCResult> => {
|
||||
try {
|
||||
await downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[app-update-handlers] Download update failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to download update'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* APP_UPDATE_INSTALL: Quit and install update
|
||||
* Quits the app and installs the downloaded update
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.APP_UPDATE_INSTALL,
|
||||
async (): Promise<IPCResult> => {
|
||||
try {
|
||||
quitAndInstall();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[app-update-handlers] Install update failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to install update'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* APP_UPDATE_GET_VERSION: Get current app version
|
||||
* Returns the current application version
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.APP_UPDATE_GET_VERSION,
|
||||
async (): Promise<string> => {
|
||||
try {
|
||||
const version = getCurrentVersion();
|
||||
return version;
|
||||
} catch (error) {
|
||||
console.error('[app-update-handlers] Get version failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('[IPC] App update handlers registered successfully');
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { registerIdeationHandlers } from './ideation-handlers';
|
||||
import { registerChangelogHandlers } from './changelog-handlers';
|
||||
import { registerInsightsHandlers } from './insights-handlers';
|
||||
import { registerDockerHandlers } from './docker-handlers';
|
||||
import { registerAppUpdateHandlers } from './app-update-handlers';
|
||||
import { notificationService } from '../notification-service';
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,9 @@ export function setupIpcHandlers(
|
||||
// Docker & infrastructure handlers (for Graphiti/FalkorDB)
|
||||
registerDockerHandlers();
|
||||
|
||||
// App auto-update handlers
|
||||
registerAppUpdateHandlers();
|
||||
|
||||
console.log('[IPC] All handler modules registered successfully');
|
||||
}
|
||||
|
||||
@@ -114,5 +118,6 @@ export {
|
||||
registerIdeationHandlers,
|
||||
registerChangelogHandlers,
|
||||
registerInsightsHandlers,
|
||||
registerDockerHandlers
|
||||
registerDockerHandlers,
|
||||
registerAppUpdateHandlers
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { IPC_CHANNELS } from '../../shared/constants';
|
||||
import type {
|
||||
AppUpdateInfo,
|
||||
AppUpdateProgress,
|
||||
AppUpdateAvailableEvent,
|
||||
AppUpdateDownloadedEvent,
|
||||
IPCResult
|
||||
} from '../../shared/types';
|
||||
import { createIpcListener, invokeIpc, IpcListenerCleanup } from './modules/ipc-utils';
|
||||
|
||||
/**
|
||||
* App Auto-Update API operations
|
||||
* Handles Electron app updates using electron-updater
|
||||
*/
|
||||
export interface AppUpdateAPI {
|
||||
// Operations
|
||||
checkAppUpdate: () => Promise<IPCResult<AppUpdateInfo | null>>;
|
||||
downloadAppUpdate: () => Promise<IPCResult>;
|
||||
installAppUpdate: () => void;
|
||||
getAppVersion: () => Promise<string>;
|
||||
|
||||
// Event Listeners
|
||||
onAppUpdateAvailable: (
|
||||
callback: (info: AppUpdateAvailableEvent) => void
|
||||
) => IpcListenerCleanup;
|
||||
onAppUpdateDownloaded: (
|
||||
callback: (info: AppUpdateDownloadedEvent) => void
|
||||
) => IpcListenerCleanup;
|
||||
onAppUpdateProgress: (
|
||||
callback: (progress: AppUpdateProgress) => void
|
||||
) => IpcListenerCleanup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the App Auto-Update API implementation
|
||||
*/
|
||||
export const createAppUpdateAPI = (): AppUpdateAPI => ({
|
||||
// Operations
|
||||
checkAppUpdate: (): Promise<IPCResult<AppUpdateInfo | null>> =>
|
||||
invokeIpc(IPC_CHANNELS.APP_UPDATE_CHECK),
|
||||
|
||||
downloadAppUpdate: (): Promise<IPCResult> =>
|
||||
invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD),
|
||||
|
||||
installAppUpdate: (): void => {
|
||||
invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL);
|
||||
},
|
||||
|
||||
getAppVersion: (): Promise<string> =>
|
||||
invokeIpc(IPC_CHANNELS.APP_UPDATE_GET_VERSION),
|
||||
|
||||
// Event Listeners
|
||||
onAppUpdateAvailable: (
|
||||
callback: (info: AppUpdateAvailableEvent) => void
|
||||
): IpcListenerCleanup =>
|
||||
createIpcListener(IPC_CHANNELS.APP_UPDATE_AVAILABLE, callback),
|
||||
|
||||
onAppUpdateDownloaded: (
|
||||
callback: (info: AppUpdateDownloadedEvent) => void
|
||||
): IpcListenerCleanup =>
|
||||
createIpcListener(IPC_CHANNELS.APP_UPDATE_DOWNLOADED, callback),
|
||||
|
||||
onAppUpdateProgress: (
|
||||
callback: (progress: AppUpdateProgress) => void
|
||||
): IpcListenerCleanup =>
|
||||
createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback)
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { SettingsAPI, createSettingsAPI } from './settings-api';
|
||||
import { FileAPI, createFileAPI } from './file-api';
|
||||
import { AgentAPI, createAgentAPI } from './agent-api';
|
||||
import { IdeationAPI, createIdeationAPI } from './modules/ideation-api';
|
||||
import { AppUpdateAPI, createAppUpdateAPI } from './app-update-api';
|
||||
|
||||
export interface ElectronAPI extends
|
||||
ProjectAPI,
|
||||
@@ -13,7 +14,8 @@ export interface ElectronAPI extends
|
||||
SettingsAPI,
|
||||
FileAPI,
|
||||
AgentAPI,
|
||||
IdeationAPI {}
|
||||
IdeationAPI,
|
||||
AppUpdateAPI {}
|
||||
|
||||
export const createElectronAPI = (): ElectronAPI => ({
|
||||
...createProjectAPI(),
|
||||
@@ -22,7 +24,8 @@ export const createElectronAPI = (): ElectronAPI => ({
|
||||
...createSettingsAPI(),
|
||||
...createFileAPI(),
|
||||
...createAgentAPI(),
|
||||
...createIdeationAPI()
|
||||
...createIdeationAPI(),
|
||||
...createAppUpdateAPI()
|
||||
});
|
||||
|
||||
// Export individual API creators for potential use in tests or specialized contexts
|
||||
@@ -33,7 +36,8 @@ export {
|
||||
createSettingsAPI,
|
||||
createFileAPI,
|
||||
createAgentAPI,
|
||||
createIdeationAPI
|
||||
createIdeationAPI,
|
||||
createAppUpdateAPI
|
||||
};
|
||||
|
||||
export type {
|
||||
@@ -43,5 +47,6 @@ export type {
|
||||
SettingsAPI,
|
||||
FileAPI,
|
||||
AgentAPI,
|
||||
IdeationAPI
|
||||
IdeationAPI,
|
||||
AppUpdateAPI
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ import { WelcomeScreen } from './components/WelcomeScreen';
|
||||
import { RateLimitModal } from './components/RateLimitModal';
|
||||
import { SDKRateLimitModal } from './components/SDKRateLimitModal';
|
||||
import { OnboardingWizard } from './components/onboarding';
|
||||
import { AppUpdateNotification } from './components/AppUpdateNotification';
|
||||
import { UsageIndicator } from './components/UsageIndicator';
|
||||
import { ProactiveSwapListener } from './components/ProactiveSwapListener';
|
||||
import { useProjectStore, loadProjects, addProject, initializeProject } from './stores/project-store';
|
||||
@@ -479,6 +480,9 @@ export function App() {
|
||||
setIsSettingsDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* App Update Notification - shows when new app version is available */}
|
||||
<AppUpdateNotification />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Download,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
ExternalLink
|
||||
} from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { Progress } from './ui/progress';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from './ui/dialog';
|
||||
import type {
|
||||
AppUpdateAvailableEvent,
|
||||
AppUpdateDownloadedEvent,
|
||||
AppUpdateProgress
|
||||
} from '../../shared/types';
|
||||
|
||||
/**
|
||||
* Simple markdown renderer for release notes
|
||||
* Handles: headers, bold, lists, line breaks
|
||||
*/
|
||||
function ReleaseNotesRenderer({ markdown }: { markdown: string }) {
|
||||
const html = useMemo(() => {
|
||||
const result = markdown
|
||||
// Escape HTML
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
// Headers (### Header -> <h3>)
|
||||
.replace(/^### (.+)$/gm, '<h4 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h4>')
|
||||
.replace(/^## (.+)$/gm, '<h3 class="text-sm font-semibold text-foreground mt-3 mb-1.5 first:mt-0">$1</h3>')
|
||||
// Bold (**text** -> <strong>)
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong class="text-foreground font-medium">$1</strong>')
|
||||
// Inline code (`code` -> <code>)
|
||||
.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 bg-muted rounded text-xs">$1</code>')
|
||||
// List items (- item -> <li>)
|
||||
.replace(/^- (.+)$/gm, '<li class="ml-4 text-muted-foreground before:content-[\'•\'] before:mr-2 before:text-muted-foreground/60">$1</li>')
|
||||
// Wrap consecutive list items
|
||||
.replace(/(<li[^>]*>.*?<\/li>\n?)+/g, '<ul class="space-y-1 my-2">$&</ul>')
|
||||
// Line breaks for remaining lines
|
||||
.replace(/\n\n/g, '<div class="h-2"></div>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
|
||||
return result;
|
||||
}, [markdown]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="text-sm text-muted-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* App Update Notification Dialog
|
||||
* Shows when a new app version is available and handles download/install workflow
|
||||
*/
|
||||
export function AppUpdateNotification() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<AppUpdateAvailableEvent | null>(null);
|
||||
const [downloadProgress, setDownloadProgress] = useState<AppUpdateProgress | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [isDownloaded, setIsDownloaded] = useState(false);
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null);
|
||||
|
||||
// Listen for update available event
|
||||
useEffect(() => {
|
||||
const cleanup = window.electronAPI.onAppUpdateAvailable((info) => {
|
||||
setUpdateInfo(info);
|
||||
setIsOpen(true);
|
||||
setIsDownloading(false);
|
||||
setIsDownloaded(false);
|
||||
setDownloadProgress(null);
|
||||
setDownloadError(null);
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
// Listen for update downloaded event
|
||||
useEffect(() => {
|
||||
const cleanup = window.electronAPI.onAppUpdateDownloaded((info) => {
|
||||
setIsDownloading(false);
|
||||
setIsDownloaded(true);
|
||||
setDownloadProgress(null);
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
// Listen for download progress
|
||||
useEffect(() => {
|
||||
const cleanup = window.electronAPI.onAppUpdateProgress((progress) => {
|
||||
setDownloadProgress(progress);
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
const handleDownload = async () => {
|
||||
setIsDownloading(true);
|
||||
setDownloadError(null);
|
||||
try {
|
||||
const result = await window.electronAPI.downloadAppUpdate();
|
||||
if (!result.success) {
|
||||
setDownloadError(result.error || 'Failed to download update');
|
||||
setIsDownloading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to download app update:', error);
|
||||
setDownloadError('Failed to download update');
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = () => {
|
||||
window.electronAPI.installAppUpdate();
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (!updateInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Download className="h-5 w-5" />
|
||||
App Update Available
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
A new version of Auto Claude is ready to download
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Version Info */}
|
||||
<div className="rounded-lg border border-border bg-muted/50 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-1">
|
||||
New Version
|
||||
</p>
|
||||
<p className="text-base font-medium text-foreground">
|
||||
{updateInfo.version}
|
||||
</p>
|
||||
{updateInfo.releaseDate && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Released {new Date(updateInfo.releaseDate).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isDownloaded ? (
|
||||
<CheckCircle2 className="h-6 w-6 text-success" />
|
||||
) : isDownloading ? (
|
||||
<RefreshCw className="h-6 w-6 animate-spin text-info" />
|
||||
) : (
|
||||
<Download className="h-6 w-6 text-info" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Release Notes */}
|
||||
{updateInfo.releaseNotes && (
|
||||
<div className="bg-background rounded-lg p-4 max-h-64 overflow-y-auto border border-border/50">
|
||||
<ReleaseNotesRenderer markdown={updateInfo.releaseNotes} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Progress */}
|
||||
{isDownloading && downloadProgress && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Downloading...</span>
|
||||
<span className="text-foreground font-medium">
|
||||
{Math.round(downloadProgress.percent)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={downloadProgress.percent} className="h-2" />
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
{(downloadProgress.transferred / 1024 / 1024).toFixed(2)} MB / {(downloadProgress.total / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Error */}
|
||||
{downloadError && (
|
||||
<div className="flex items-center gap-3 text-sm text-destructive bg-destructive/10 border border-destructive/30 rounded-lg p-3">
|
||||
<AlertCircle className="h-5 w-5 shrink-0" />
|
||||
<span>{downloadError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Downloaded Success */}
|
||||
{isDownloaded && (
|
||||
<div className="flex items-center gap-3 text-sm text-success bg-success/10 border border-success/30 rounded-lg p-3">
|
||||
<CheckCircle2 className="h-5 w-5 shrink-0" />
|
||||
<span>Update downloaded successfully! Click Install to restart and apply the update.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDismiss}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
{isDownloaded ? 'Install Later' : 'Remind Me Later'}
|
||||
</Button>
|
||||
|
||||
{isDownloaded ? (
|
||||
<Button onClick={handleInstall}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Install and Restart
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
Downloading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Download Update
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -257,5 +257,17 @@ export const IPC_CHANNELS = {
|
||||
// Git operations
|
||||
GIT_GET_BRANCHES: 'git:getBranches',
|
||||
GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch',
|
||||
GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch'
|
||||
GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch',
|
||||
|
||||
// App auto-update operations
|
||||
APP_UPDATE_CHECK: 'app-update:check',
|
||||
APP_UPDATE_DOWNLOAD: 'app-update:download',
|
||||
APP_UPDATE_INSTALL: 'app-update:install',
|
||||
APP_UPDATE_GET_VERSION: 'app-update:get-version',
|
||||
|
||||
// App auto-update events (main -> renderer)
|
||||
APP_UPDATE_AVAILABLE: 'app-update:available',
|
||||
APP_UPDATE_DOWNLOADED: 'app-update:downloaded',
|
||||
APP_UPDATE_PROGRESS: 'app-update:progress',
|
||||
APP_UPDATE_ERROR: 'app-update:error'
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Types for Electron app auto-update functionality
|
||||
*/
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
version: string;
|
||||
releaseNotes?: string;
|
||||
releaseDate?: string;
|
||||
}
|
||||
|
||||
export interface AppUpdateProgress {
|
||||
percent: number;
|
||||
transferred: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface AppUpdateAvailableEvent {
|
||||
version: string;
|
||||
releaseNotes?: string;
|
||||
releaseDate?: string;
|
||||
}
|
||||
|
||||
export interface AppUpdateDownloadedEvent {
|
||||
version: string;
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export * from './changelog';
|
||||
export * from './insights';
|
||||
export * from './roadmap';
|
||||
export * from './integrations';
|
||||
export * from './glossary';
|
||||
export * from './status';
|
||||
export * from './app-update';
|
||||
|
||||
// IPC types (must be last to use types from other modules)
|
||||
export * from './ipc';
|
||||
|
||||
Reference in New Issue
Block a user