From 1e3e8bda1deadff74811af7abd81788f876e3a97 Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:00:20 +0100 Subject: [PATCH] Fix/update app (#594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cleanup/readme * fix(updater): remove redundant source updater and add beta→stable downgrade The app had two update systems: electron-updater (correct) and a redundant "source updater" that caused version desync. After updating, getEffectiveVersion() checked stale .update-metadata.json first, showing wrong version numbers. Changes: - Remove redundant auto-claude-updater and source update handlers - Clean up stale metadata directories on app startup - Use app.getVersion() directly for version display - Add beta→stable downgrade when user disables beta updates - Fetch latest stable from GitHub API and offer to install 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(updater): enable stable version downgrade with allowDowngrade flag Fixes critical issue where electron-updater's semver comparison prevented downloading older stable versions when on a beta release. Also addresses several robustness issues in the update mechanism: - Set allowDowngrade=true in downloadStableVersion() to enable downgrades - Add dedicated IPC channel APP_UPDATE_DOWNLOAD_STABLE for stable downloads - Add HTTP status code validation to GitHub API requests - Add 10-second timeout to prevent hanging requests - Add JSON array validation before processing releases - Fix fire-and-forget async call with proper error handling - Fix UI handlers to check IPCResult and reset loading state on failure - Clear beta update info when disabling beta so stable downgrade UI shows --------- Co-authored-by: Claude Opus 4.5 --- README.md | 115 +----- apps/frontend/src/main/app-updater.ts | 204 +++++++++- apps/frontend/src/main/auto-claude-updater.ts | 48 --- apps/frontend/src/main/index.ts | 32 +- .../main/ipc-handlers/app-update-handlers.ts | 21 ++ .../ipc-handlers/autobuild-source-handlers.ts | 321 ---------------- apps/frontend/src/main/ipc-handlers/index.ts | 5 - .../main/ipc-handlers/settings-handlers.ts | 19 +- .../ipc-handlers/task/worktree-handlers.ts | 2 +- apps/frontend/src/main/updater/config.ts | 30 -- .../src/main/updater/file-operations.ts | 135 ------- apps/frontend/src/main/updater/http-client.ts | 189 ---------- apps/frontend/src/main/updater/types.ts | 63 ---- .../src/main/updater/update-checker.ts | 77 ---- .../src/main/updater/update-installer.ts | 224 ----------- .../src/main/updater/update-status.ts | 51 --- .../src/main/updater/version-manager.ts | 84 +---- apps/frontend/src/preload/api/agent-api.ts | 8 - .../src/preload/api/app-update-api.ts | 14 +- .../src/preload/api/modules/autobuild-api.ts | 43 --- .../frontend/src/preload/api/modules/index.ts | 1 - .../components/settings/AdvancedSettings.tsx | 349 +++++++++--------- .../renderer/lib/mocks/infrastructure-mock.ts | 22 -- .../src/renderer/lib/mocks/settings-mock.ts | 4 +- apps/frontend/src/shared/constants/ipc.ts | 8 +- .../src/shared/i18n/locales/en/settings.json | 6 +- .../src/shared/i18n/locales/fr/settings.json | 6 +- apps/frontend/src/shared/types/ipc.ts | 16 +- apps/frontend/src/shared/types/settings.ts | 24 -- guides/CLI-USAGE.md | 28 ++ guides/linux.md | 95 +++++ 31 files changed, 612 insertions(+), 1632 deletions(-) delete mode 100644 apps/frontend/src/main/auto-claude-updater.ts delete mode 100644 apps/frontend/src/main/ipc-handlers/autobuild-source-handlers.ts delete mode 100644 apps/frontend/src/main/updater/config.ts delete mode 100644 apps/frontend/src/main/updater/file-operations.ts delete mode 100644 apps/frontend/src/main/updater/http-client.ts delete mode 100644 apps/frontend/src/main/updater/types.ts delete mode 100644 apps/frontend/src/main/updater/update-checker.ts delete mode 100644 apps/frontend/src/main/updater/update-installer.ts delete mode 100644 apps/frontend/src/main/updater/update-status.ts delete mode 100644 apps/frontend/src/preload/api/modules/autobuild-api.ts create mode 100644 guides/linux.md diff --git a/README.md b/README.md index f79a9bcf..b1c8a3ac 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,9 @@ ![Auto Claude Kanban Board](.github/assets/Auto-Claude-Kanban.png) - -[![Version](https://img.shields.io/badge/version-2.7.2-blue?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.2) - [![License](https://img.shields.io/badge/license-AGPL--3.0-green?style=flat-square)](./agpl-3.0.txt) [![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/KCXaPBr4Dj) +[![YouTube](https://img.shields.io/badge/YouTube-Subscribe-FF0000?style=flat-square&logo=youtube&logoColor=white)](https://www.youtube.com/@AndreMikalsen) [![CI](https://img.shields.io/github/actions/workflow/status/AndyMik90/Auto-Claude/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/AndyMik90/Auto-Claude/actions) --- @@ -59,7 +57,6 @@ - **Claude Pro/Max subscription** - [Get one here](https://claude.ai/upgrade) - **Claude Code CLI** - `npm install -g @anthropic-ai/claude-code` - **Git repository** - Your project must be initialized as a git repo -- **Python 3.12+** - Required for the backend and Memory Layer --- @@ -148,113 +145,11 @@ See [guides/CLI-USAGE.md](guides/CLI-USAGE.md) for complete CLI documentation. --- -## Configuration +## Development -Create `apps/backend/.env` from the example: +Want to build from source or contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) for complete development setup instructions. -```bash -cp apps/backend/.env.example apps/backend/.env -``` - -| Variable | Required | Description | -|----------|----------|-------------| -| `CLAUDE_CODE_OAUTH_TOKEN` | Yes | OAuth token from `claude setup-token` | -| `GRAPHITI_ENABLED` | No | Enable Memory Layer for cross-session context | -| `AUTO_BUILD_MODEL` | No | Override the default Claude model | -| `GITLAB_TOKEN` | No | GitLab Personal Access Token for GitLab integration | -| `GITLAB_INSTANCE_URL` | No | GitLab instance URL (defaults to gitlab.com) | -| `LINEAR_API_KEY` | No | Linear API key for task sync | - ---- - -## Building from Source - -For contributors and development: - -```bash -# Clone the repository -git clone https://github.com/AndyMik90/Auto-Claude.git -cd Auto-Claude - -# Install all dependencies -npm run install:all - -# Run in development mode -npm run dev - -# Or build and run -npm start -``` - -**System requirements for building:** -- Node.js 24+ -- Python 3.12+ -- npm 10+ - -**Installing dependencies by platform:** - -
-Windows - -```bash -winget install Python.Python.3.12 -winget install OpenJS.NodeJS.LTS -``` - -
- -
-macOS - -```bash -brew install python@3.12 node@24 -``` - -
- -
-Linux (Ubuntu/Debian) - -```bash -sudo apt install python3.12 python3.12-venv -curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - -sudo apt install -y nodejs -``` - -
- -
-Linux (Fedora) - -```bash -sudo dnf install python3.12 nodejs npm -``` - -
- -See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed development setup. - -### Building Flatpak - -To build the Flatpak package, you need additional dependencies: - -```bash -# Fedora/RHEL -sudo dnf install flatpak-builder - -# Ubuntu/Debian -sudo apt install flatpak-builder - -# Install required Flatpak runtimes -flatpak install flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 -flatpak install flathub org.electronjs.Electron2.BaseApp//25.08 - -# Build the Flatpak -cd apps/frontend -npm run package:flatpak -``` - -The Flatpak will be created in `apps/frontend/dist/`. +For Linux-specific builds (Flatpak, AppImage), see [guides/linux.md](guides/linux.md). --- @@ -284,7 +179,7 @@ All releases are: | `npm run package:mac` | Package for macOS | | `npm run package:win` | Package for Windows | | `npm run package:linux` | Package for Linux | -| `npm run package:flatpak` | Package as Flatpak | +| `npm run package:flatpak` | Package as Flatpak (see [guides/linux.md](guides/linux.md)) | | `npm run lint` | Run linter | | `npm test` | Run frontend tests | | `npm run test:backend` | Run backend tests | diff --git a/apps/frontend/src/main/app-updater.ts b/apps/frontend/src/main/app-updater.ts index a76444dd..c1620b43 100644 --- a/apps/frontend/src/main/app-updater.ts +++ b/apps/frontend/src/main/app-updater.ts @@ -18,12 +18,16 @@ */ import { autoUpdater } from 'electron-updater'; -import { app } from 'electron'; +import { app, net } from 'electron'; import type { BrowserWindow } from 'electron'; import { IPC_CHANNELS } from '../shared/constants'; import type { AppUpdateInfo } from '../shared/types'; import { compareVersions } from './updater/version-manager'; +// GitHub repo info for API calls +const GITHUB_OWNER = 'AndyMik90'; +const GITHUB_REPO = 'Auto-Claude'; + // Debug mode - DEBUG_UPDATER=true or development mode const DEBUG_UPDATER = process.env.DEBUG_UPDATER === 'true' || process.env.NODE_ENV === 'development'; @@ -251,3 +255,201 @@ export function quitAndInstall(): void { export function getCurrentVersion(): string { return autoUpdater.currentVersion.version; } + +/** + * Check if a version string represents a prerelease (beta, alpha, rc, etc.) + */ +export function isPrerelease(version: string): boolean { + return /-(alpha|beta|rc|dev|canary)\.\d+$/i.test(version) || version.includes('-'); +} + +// Timeout for GitHub API requests (10 seconds) +const GITHUB_API_TIMEOUT = 10000; + +/** + * Fetch the latest stable release from GitHub API + * Returns the latest non-prerelease version + */ +async function fetchLatestStableRelease(): Promise { + const fetchPromise = new Promise((resolve) => { + const url = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases`; + console.warn('[app-updater] Fetching releases from:', url); + + const request = net.request({ + url, + method: 'GET' + }); + + request.setHeader('Accept', 'application/vnd.github.v3+json'); + request.setHeader('User-Agent', `Auto-Claude/${getCurrentVersion()}`); + + let data = ''; + + request.on('response', (response) => { + // Validate HTTP status code + const statusCode = response.statusCode; + if (statusCode !== 200) { + console.error(`[app-updater] GitHub API error: HTTP ${statusCode}`); + if (statusCode === 403) { + console.error('[app-updater] Rate limit may have been exceeded'); + } else if (statusCode === 404) { + console.error('[app-updater] Repository or releases not found'); + } + resolve(null); + return; + } + + response.on('data', (chunk) => { + data += chunk.toString(); + }); + + response.on('end', () => { + try { + const parsed = JSON.parse(data); + + // Validate response is an array + if (!Array.isArray(parsed)) { + console.error('[app-updater] Unexpected response format - expected array, got:', typeof parsed); + resolve(null); + return; + } + + const releases = parsed as Array<{ + tag_name: string; + prerelease: boolean; + draft: boolean; + body?: string; + published_at?: string; + html_url?: string; + }>; + + // Find the first non-prerelease, non-draft release + const latestStable = releases.find(r => !r.prerelease && !r.draft); + + if (!latestStable) { + console.warn('[app-updater] No stable release found'); + resolve(null); + return; + } + + const version = latestStable.tag_name.replace(/^v/, ''); + console.warn('[app-updater] Found latest stable release:', version); + + resolve({ + version, + releaseNotes: latestStable.body, + releaseDate: latestStable.published_at + }); + } catch (e) { + console.error('[app-updater] Failed to parse releases JSON:', e); + resolve(null); + } + }); + }); + + request.on('error', (error) => { + console.error('[app-updater] Failed to fetch releases:', error); + resolve(null); + }); + + request.end(); + }); + + // Add timeout to prevent hanging indefinitely + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + console.error(`[app-updater] GitHub API request timed out after ${GITHUB_API_TIMEOUT}ms`); + resolve(null); + }, GITHUB_API_TIMEOUT); + }); + + return Promise.race([fetchPromise, timeoutPromise]); +} + +/** + * Check if we should offer a downgrade to stable + * Called when user disables beta updates while on a prerelease version + * + * Returns the latest stable version if: + * 1. Current version is a prerelease + * 2. A stable version exists + */ +export async function checkForStableDowngrade(): Promise { + const currentVersion = getCurrentVersion(); + + // Only check for downgrade if currently on a prerelease + if (!isPrerelease(currentVersion)) { + console.warn('[app-updater] Current version is not a prerelease, no downgrade needed'); + return null; + } + + console.warn('[app-updater] Current version is prerelease:', currentVersion); + console.warn('[app-updater] Checking for stable version to downgrade to...'); + + const latestStable = await fetchLatestStableRelease(); + + if (!latestStable) { + console.warn('[app-updater] No stable release available for downgrade'); + return null; + } + + console.warn('[app-updater] Stable downgrade available:', latestStable.version); + return latestStable; +} + +/** + * Set update channel with optional downgrade check + * When switching from beta to stable, checks if user should be offered a downgrade + * + * @param channel - The update channel to switch to + * @param triggerDowngradeCheck - Whether to check for stable downgrade (when disabling beta) + */ +export async function setUpdateChannelWithDowngradeCheck( + channel: UpdateChannel, + triggerDowngradeCheck = false +): Promise { + autoUpdater.channel = channel; + console.warn(`[app-updater] Update channel set to: ${channel}`); + + // If switching to stable and downgrade check requested, look for stable version + if (channel === 'latest' && triggerDowngradeCheck) { + const stableVersion = await checkForStableDowngrade(); + + if (stableVersion && mainWindow) { + // Notify the renderer about the available stable downgrade + mainWindow.webContents.send(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, stableVersion); + } + + return stableVersion; + } + + return null; +} + +/** + * Download a specific version (for downgrade) + * Uses electron-updater with allowDowngrade enabled to download older stable versions + */ +export async function downloadStableVersion(): Promise { + // Switch to stable channel + autoUpdater.channel = 'latest'; + // Enable downgrade to allow downloading older versions (e.g., stable when on beta) + autoUpdater.allowDowngrade = true; + console.warn('[app-updater] Downloading stable version (allowDowngrade=true)...'); + + try { + // Force a fresh check on the stable channel, then download + const result = await autoUpdater.checkForUpdates(); + if (result) { + await autoUpdater.downloadUpdate(); + } else { + throw new Error('No stable version available for download'); + } + } catch (error) { + console.error('[app-updater] Failed to download stable version:', error); + throw error; + } finally { + // Reset allowDowngrade to prevent unintended downgrades in normal update checks + autoUpdater.allowDowngrade = false; + } +} diff --git a/apps/frontend/src/main/auto-claude-updater.ts b/apps/frontend/src/main/auto-claude-updater.ts deleted file mode 100644 index b19e1985..00000000 --- a/apps/frontend/src/main/auto-claude-updater.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Auto Claude Source Updater - * - * Checks GitHub Releases for updates and downloads them. - * GitHub Releases are the single source of truth for versioning. - * - * Update flow: - * 1. Check GitHub Releases API for the latest release - * 2. Compare release tag with current app version - * 3. If update available, download release tarball and apply - * 4. Existing project update system handles pushing to individual projects - * - * Versioning: - * - Single source of truth: GitHub Releases - * - Current version: app.getVersion() (from package.json at build time) - * - Latest version: Fetched from GitHub Releases API - * - To release: Create a GitHub release with tag (e.g., v1.2.0) - */ - -// Export types -export type { - GitHubRelease, - AutoBuildUpdateCheck, - AutoBuildUpdateResult, - UpdateProgressCallback, - UpdateMetadata -} from './updater/types'; - -// Export version management -export { getBundledVersion, getEffectiveVersion } from './updater/version-manager'; - -// Export path resolution -export { - getBundledSourcePath, - getEffectiveSourcePath -} from './updater/path-resolver'; - -// Export update checking -export { checkForUpdates } from './updater/update-checker'; - -// Export update installation -export { downloadAndApplyUpdate } from './updater/update-installer'; - -// Export update status -export { - hasPendingSourceUpdate, - getUpdateMetadata -} from './updater/update-status'; diff --git a/apps/frontend/src/main/index.ts b/apps/frontend/src/main/index.ts index e7460a46..3a96c241 100644 --- a/apps/frontend/src/main/index.ts +++ b/apps/frontend/src/main/index.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow, shell, nativeImage, session } from 'electron'; import { join } from 'path'; -import { accessSync, readFileSync, writeFileSync } from 'fs'; +import { accessSync, readFileSync, writeFileSync, rmSync, existsSync } from 'fs'; import { electronApp, optimizer, is } from '@electron-toolkit/utils'; import { setupIpcHandlers } from './ipc-setup'; import { AgentManager } from './agent'; @@ -30,6 +30,32 @@ function loadSettingsSync(): AppSettings { return { ...DEFAULT_APP_SETTINGS, ...savedSettings } as AppSettings; } +/** + * Clean up stale update metadata files from the redundant source updater system. + * + * The old "source updater" wrote .update-metadata.json files that could persist + * across app updates and cause version display desync. This cleanup ensures + * we use the actual bundled version from app.getVersion(). + */ +function cleanupStaleUpdateMetadata(): void { + const userData = app.getPath('userData'); + const stalePaths = [ + join(userData, 'auto-claude-source'), + join(userData, 'backend-source'), + ]; + + for (const stalePath of stalePaths) { + if (existsSync(stalePath)) { + try { + rmSync(stalePath, { recursive: true, force: true }); + console.warn(`[main] Cleaned up stale update metadata: ${stalePath}`); + } catch (e) { + console.warn(`[main] Failed to clean up stale metadata at ${stalePath}:`, e); + } + } + } +} + // Get icon path based on platform function getIconPath(): string { // In dev mode, __dirname is out/main, so we go up to project root then into resources @@ -133,6 +159,10 @@ app.whenReady().then(() => { .catch((err) => console.warn('[main] Failed to clear cache:', err)); } + // Clean up stale update metadata from the old source updater system + // This prevents version display desync after electron-updater installs a new version + cleanupStaleUpdateMetadata(); + // Set dock icon on macOS if (process.platform === 'darwin') { const iconPath = getIconPath(); diff --git a/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts b/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts index 1d0b963e..66c7f3ee 100644 --- a/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/app-update-handlers.ts @@ -11,6 +11,7 @@ import type { IPCResult, AppUpdateInfo } from '../../shared/types'; import { checkForUpdates, downloadUpdate, + downloadStableVersion, quitAndInstall, getCurrentVersion } from '../app-updater'; @@ -65,6 +66,26 @@ export function registerAppUpdateHandlers(): void { } ); + /** + * APP_UPDATE_DOWNLOAD_STABLE: Download stable version (for downgrade from beta) + * Uses allowDowngrade to download an older stable version + */ + ipcMain.handle( + IPC_CHANNELS.APP_UPDATE_DOWNLOAD_STABLE, + async (): Promise => { + try { + await downloadStableVersion(); + return { success: true }; + } catch (error) { + console.error('[app-update-handlers] Download stable version failed:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to download stable version' + }; + } + } + ); + /** * APP_UPDATE_INSTALL: Quit and install update * Quits the app and installs the downloaded update diff --git a/apps/frontend/src/main/ipc-handlers/autobuild-source-handlers.ts b/apps/frontend/src/main/ipc-handlers/autobuild-source-handlers.ts deleted file mode 100644 index 4a4ab66d..00000000 --- a/apps/frontend/src/main/ipc-handlers/autobuild-source-handlers.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { ipcMain } from 'electron'; -import type { BrowserWindow } from 'electron'; -import { IPC_CHANNELS } from '../../shared/constants'; -import type { IPCResult } from '../../shared/types'; -import path from 'path'; -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import type { AutoBuildSourceUpdateProgress, SourceEnvConfig, SourceEnvCheckResult } from '../../shared/types'; -import { checkForUpdates as checkSourceUpdates, downloadAndApplyUpdate, getBundledVersion, getEffectiveVersion, getEffectiveSourcePath } from '../auto-claude-updater'; -import { debugLog } from '../../shared/utils/debug-logger'; - - -/** - * Register all autobuild-source-related IPC handlers - */ -export function registerAutobuildSourceHandlers( - getMainWindow: () => BrowserWindow | null -): void { - // ============================================ - // Auto Claude Source Update Operations - // ============================================ - - ipcMain.handle( - IPC_CHANNELS.AUTOBUILD_SOURCE_CHECK, - async (): Promise> => { - console.log('[autobuild-source] Check for updates called'); - debugLog('[IPC] AUTOBUILD_SOURCE_CHECK called'); - try { - const result = await checkSourceUpdates(); - console.log('[autobuild-source] Check result:', JSON.stringify(result, null, 2)); - debugLog('[IPC] AUTOBUILD_SOURCE_CHECK result:', result); - return { success: true, data: result }; - } catch (error) { - console.error('[autobuild-source] Check error:', error); - debugLog('[IPC] AUTOBUILD_SOURCE_CHECK error:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to check for updates' - }; - } - } - ); - - ipcMain.on( - IPC_CHANNELS.AUTOBUILD_SOURCE_DOWNLOAD, - () => { - debugLog('[IPC] Autobuild source download requested'); - const mainWindow = getMainWindow(); - if (!mainWindow) { - debugLog('[IPC] No main window available, aborting update'); - return; - } - - // Start download in background - downloadAndApplyUpdate((progress) => { - debugLog('[IPC] Update progress:', progress.stage, progress.message); - mainWindow.webContents.send( - IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, - progress - ); - }).then((result) => { - if (result.success) { - debugLog('[IPC] Update completed successfully, version:', result.version); - mainWindow.webContents.send( - IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, - { - stage: 'complete', - message: `Updated to version ${result.version}`, - newVersion: result.version // Include new version for UI refresh - } as AutoBuildSourceUpdateProgress - ); - } else { - debugLog('[IPC] Update failed:', result.error); - mainWindow.webContents.send( - IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, - { - stage: 'error', - message: result.error || 'Update failed' - } as AutoBuildSourceUpdateProgress - ); - } - }).catch((error) => { - debugLog('[IPC] Update error:', error instanceof Error ? error.message : error); - mainWindow.webContents.send( - IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, - { - stage: 'error', - message: error instanceof Error ? error.message : 'Update failed' - } as AutoBuildSourceUpdateProgress - ); - }); - - // Send initial progress - mainWindow.webContents.send( - IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, - { - stage: 'checking', - message: 'Starting update...' - } as AutoBuildSourceUpdateProgress - ); - } - ); - - ipcMain.handle( - IPC_CHANNELS.AUTOBUILD_SOURCE_VERSION, - async (): Promise> => { - try { - // Use effective version which accounts for source updates - const version = getEffectiveVersion(); - debugLog('[IPC] Returning effective version:', version); - return { success: true, data: version }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to get version' - }; - } - } - ); - - // ============================================ - // Auto Claude Source Environment Operations - // ============================================ - - /** - * Parse an .env file content into a key-value object - */ - const parseSourceEnvFile = (content: string): Record => { - const vars: Record = {}; - for (const line of content.split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - - const eqIndex = trimmed.indexOf('='); - if (eqIndex > 0) { - const key = trimmed.substring(0, eqIndex).trim(); - let value = trimmed.substring(eqIndex + 1).trim(); - // Remove quotes if present - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - vars[key] = value; - } - } - return vars; - }; - - ipcMain.handle( - IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET, - async (): Promise> => { - try { - const sourcePath = getEffectiveSourcePath(); - if (!sourcePath) { - return { - success: true, - data: { - hasClaudeToken: false, - envExists: false, - sourcePath: undefined - } - }; - } - - const envPath = path.join(sourcePath, '.env'); - const envExists = existsSync(envPath); - - if (!envExists) { - return { - success: true, - data: { - hasClaudeToken: false, - envExists: false, - sourcePath - } - }; - } - - const content = readFileSync(envPath, 'utf-8'); - const vars = parseSourceEnvFile(content); - const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN']; - - return { - success: true, - data: { - hasClaudeToken: hasToken, - claudeOAuthToken: hasToken ? vars['CLAUDE_CODE_OAUTH_TOKEN'] : undefined, - envExists: true, - sourcePath - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to get source env' - }; - } - } - ); - - ipcMain.handle( - IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE, - async (_, config: { claudeOAuthToken?: string }): Promise => { - try { - const sourcePath = getEffectiveSourcePath(); - if (!sourcePath) { - return { - success: false, - error: 'Auto-Claude source path not found. Please configure it in App Settings.' - }; - } - - const envPath = path.join(sourcePath, '.env'); - - // Read existing content or start fresh - let existingContent = ''; - const existingVars: Record = {}; - - if (existsSync(envPath)) { - existingContent = readFileSync(envPath, 'utf-8'); - Object.assign(existingVars, parseSourceEnvFile(existingContent)); - } - - // Update the token - if (config.claudeOAuthToken !== undefined) { - existingVars['CLAUDE_CODE_OAUTH_TOKEN'] = config.claudeOAuthToken; - } - - // Rebuild the .env file preserving comments and structure - const lines = existingContent.split('\n'); - const processedKeys = new Set(); - const outputLines: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { - outputLines.push(line); - continue; - } - - const eqIndex = trimmed.indexOf('='); - if (eqIndex > 0) { - const key = trimmed.substring(0, eqIndex).trim(); - if (key in existingVars) { - outputLines.push(`${key}=${existingVars[key]}`); - processedKeys.add(key); - } else { - outputLines.push(line); - } - } else { - outputLines.push(line); - } - } - - // Add any new keys that weren't in the original file - for (const [key, value] of Object.entries(existingVars)) { - if (!processedKeys.has(key)) { - outputLines.push(`${key}=${value}`); - } - } - - writeFileSync(envPath, outputLines.join('\n')); - - return { success: true }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update source env' - }; - } - } - ); - - ipcMain.handle( - IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN, - async (): Promise> => { - try { - const sourcePath = getEffectiveSourcePath(); - if (!sourcePath) { - return { - success: true, - data: { - hasToken: false, - sourcePath: undefined, - error: 'Auto-Claude source path not found' - } - }; - } - - const envPath = path.join(sourcePath, '.env'); - if (!existsSync(envPath)) { - return { - success: true, - data: { - hasToken: false, - sourcePath, - error: '.env file does not exist' - } - }; - } - - const content = readFileSync(envPath, 'utf-8'); - const vars = parseSourceEnvFile(content); - const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'] && vars['CLAUDE_CODE_OAUTH_TOKEN'].length > 0; - - return { - success: true, - data: { - hasToken, - sourcePath - } - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to check source token' - }; - } - } - ); - -} diff --git a/apps/frontend/src/main/ipc-handlers/index.ts b/apps/frontend/src/main/ipc-handlers/index.ts index 22549de7..b3ee5721 100644 --- a/apps/frontend/src/main/ipc-handlers/index.ts +++ b/apps/frontend/src/main/ipc-handlers/index.ts @@ -23,7 +23,6 @@ import { registerEnvHandlers } from './env-handlers'; import { registerLinearHandlers } from './linear-handlers'; import { registerGithubHandlers } from './github-handlers'; import { registerGitlabHandlers } from './gitlab-handlers'; -import { registerAutobuildSourceHandlers } from './autobuild-source-handlers'; import { registerIdeationHandlers } from './ideation-handlers'; import { registerChangelogHandlers } from './changelog-handlers'; import { registerInsightsHandlers } from './insights-handlers'; @@ -92,9 +91,6 @@ export function setupIpcHandlers( // GitLab integration handlers registerGitlabHandlers(agentManager, getMainWindow); - // Auto-build source update handlers - registerAutobuildSourceHandlers(getMainWindow); - // Ideation handlers registerIdeationHandlers(agentManager, getMainWindow); @@ -140,7 +136,6 @@ export { registerLinearHandlers, registerGithubHandlers, registerGitlabHandlers, - registerAutobuildSourceHandlers, registerIdeationHandlers, registerChangelogHandlers, registerInsightsHandlers, diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index d6e7b94f..00fd8429 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -10,8 +10,7 @@ import type { } from '../../shared/types'; import { AgentManager } from '../agent'; import type { BrowserWindow } from 'electron'; -import { getEffectiveVersion } from '../auto-claude-updater'; -import { setUpdateChannel } from '../app-updater'; +import { setUpdateChannel, setUpdateChannelWithDowngradeCheck } from '../app-updater'; import { getSettingsPath, readSettingsFile } from '../settings-utils'; import { configureTools, getToolPath, getToolInfo, isPathFromWrongPlatform } from '../cli-tool-manager'; @@ -211,8 +210,16 @@ export function registerSettingsHandlers( // Update auto-updater channel if betaUpdates setting changed if (settings.betaUpdates !== undefined) { - const channel = settings.betaUpdates ? 'beta' : 'latest'; - setUpdateChannel(channel); + if (settings.betaUpdates) { + // Enabling beta updates - just switch channel + setUpdateChannel('beta'); + } else { + // Disabling beta updates - switch to stable and check if downgrade is available + // This will notify the renderer if user is on a prerelease and stable version exists + setUpdateChannelWithDowngradeCheck('latest', true).catch((err) => { + console.error('[settings-handlers] Failed to check for stable downgrade:', err); + }); + } } return { success: true }; @@ -372,8 +379,8 @@ export function registerSettingsHandlers( // ============================================ ipcMain.handle(IPC_CHANNELS.APP_VERSION, async (): Promise => { - // Use effective version which accounts for source updates - const version = getEffectiveVersion(); + // Return the actual bundled version from package.json + const version = app.getVersion(); console.log('[settings-handlers] APP_VERSION returning:', version); return version; }); diff --git a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts index 9ff9088b..7a9cd8e7 100644 --- a/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/worktree-handlers.ts @@ -6,7 +6,7 @@ import { existsSync, readdirSync, statSync, readFileSync } from 'fs'; import { execSync, execFileSync, spawn, spawnSync, exec, execFile } from 'child_process'; import { projectStore } from '../../project-store'; import { getConfiguredPythonPath, PythonEnvManager, pythonEnvManager as pythonEnvManagerSingleton } from '../../python-env-manager'; -import { getEffectiveSourcePath } from '../../auto-claude-updater'; +import { getEffectiveSourcePath } from '../../updater/path-resolver'; import { getProfileEnv } from '../../rate-limit-detector'; import { findTaskAndProject } from './shared'; import { parsePythonCommand } from '../../python-detector'; diff --git a/apps/frontend/src/main/updater/config.ts b/apps/frontend/src/main/updater/config.ts deleted file mode 100644 index 982042a6..00000000 --- a/apps/frontend/src/main/updater/config.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Configuration for Auto Claude updater - */ - -/** - * GitHub repository configuration - */ -export const GITHUB_CONFIG = { - owner: 'AndyMik90', - repo: 'Auto-Claude', - autoBuildPath: 'apps/backend' // Path within repo where auto-claude backend lives -} as const; - -/** - * Files and directories to preserve during updates - */ -export const PRESERVE_FILES = ['.env', 'specs'] as const; - -/** - * Files and directories to skip when copying - */ -export const SKIP_FILES = ['__pycache__', '.DS_Store', '.git', 'specs', '.env'] as const; - -/** - * Update-related timeouts (in milliseconds) - */ -export const TIMEOUTS = { - requestTimeout: 10000, - downloadTimeout: 60000 -} as const; diff --git a/apps/frontend/src/main/updater/file-operations.ts b/apps/frontend/src/main/updater/file-operations.ts deleted file mode 100644 index b948631d..00000000 --- a/apps/frontend/src/main/updater/file-operations.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * File operation utilities for updates - */ - -import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs'; -import path from 'path'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { SKIP_FILES } from './config'; - -const execAsync = promisify(exec); - -/** - * Extract a .tar.gz file - * Uses system tar command on Unix or PowerShell on Windows - */ -export async function extractTarball(tarballPath: string, destPath: string): Promise { - try { - if (process.platform === 'win32') { - // On Windows, try multiple approaches: - // 1. Modern Windows 10/11 has built-in tar - // 2. Fall back to PowerShell's Expand-Archive for .zip (but .tar.gz needs tar) - // 3. Use PowerShell to extract via .NET - try { - // First try native tar (available on Windows 10 1803+) - await execAsync(`tar -xzf "${tarballPath}" -C "${destPath}"`); - } catch { - // Fall back to PowerShell with .NET for gzip decompression - // This is more complex but works on older Windows versions - const psScript = ` - $tarball = "${tarballPath.replace(/\\/g, '\\\\')}" - $dest = "${destPath.replace(/\\/g, '\\\\')}" - $tempTar = Join-Path $env:TEMP "auto-claude-update.tar" - - # Decompress gzip - $gzipStream = [System.IO.File]::OpenRead($tarball) - $decompressedStream = New-Object System.IO.Compression.GZipStream($gzipStream, [System.IO.Compression.CompressionMode]::Decompress) - $tarStream = [System.IO.File]::Create($tempTar) - $decompressedStream.CopyTo($tarStream) - $tarStream.Close() - $decompressedStream.Close() - $gzipStream.Close() - - # Extract tar using tar command (should work even if gzip didn't) - tar -xf $tempTar -C $dest - Remove-Item $tempTar -Force - `; - await execAsync(`powershell -NoProfile -Command "${psScript.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`); - } - } else { - // Unix systems - use native tar - await execAsync(`tar -xzf "${tarballPath}" -C "${destPath}"`); - } - } catch (error) { - throw new Error(`Failed to extract tarball: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -} - -/** - * Recursively copy directory - */ -export function copyDirectoryRecursive( - src: string, - dest: string, - preserveExisting: boolean = false -): void { - if (!existsSync(dest)) { - mkdirSync(dest, { recursive: true }); - } - - const entries = readdirSync(src, { withFileTypes: true }); - - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - - // Skip certain files/directories - if (SKIP_FILES.includes(entry.name as (typeof SKIP_FILES)[number])) { - continue; - } - - // In preserve mode, skip existing files - if (preserveExisting && existsSync(destPath)) { - if (entry.isDirectory()) { - copyDirectoryRecursive(srcPath, destPath, preserveExisting); - } - continue; - } - - if (entry.isDirectory()) { - copyDirectoryRecursive(srcPath, destPath, preserveExisting); - } else { - copyFileSync(srcPath, destPath); - } - } -} - -/** - * Preserve specified files before update - */ -export function preserveFiles(targetPath: string, filesToPreserve: readonly string[]): Record { - const preservedContent: Record = {}; - - for (const file of filesToPreserve) { - const filePath = path.join(targetPath, file); - if (existsSync(filePath)) { - if (!statSync(filePath).isDirectory()) { - preservedContent[file] = readFileSync(filePath); - } - } - } - - return preservedContent; -} - -/** - * Restore preserved files after update - */ -export function restoreFiles(targetPath: string, preservedContent: Record): void { - for (const [file, content] of Object.entries(preservedContent)) { - writeFileSync(path.join(targetPath, file), content); - } -} - -/** - * Clean target directory while preserving specified files - */ -export function cleanTargetDirectory(targetPath: string, preserveFiles: readonly string[]): void { - const items = readdirSync(targetPath); - for (const item of items) { - if (!preserveFiles.includes(item)) { - rmSync(path.join(targetPath, item), { recursive: true, force: true }); - } - } -} diff --git a/apps/frontend/src/main/updater/http-client.ts b/apps/frontend/src/main/updater/http-client.ts deleted file mode 100644 index ada5f5d4..00000000 --- a/apps/frontend/src/main/updater/http-client.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * HTTP client utilities for fetching updates - */ - -import https from 'https'; -import { createWriteStream } from 'fs'; -import { TIMEOUTS, GITHUB_CONFIG } from './config'; - -/** - * Fetch JSON from a URL using https - */ -export function fetchJson(url: string): Promise { - return new Promise((resolve, reject) => { - const headers = { - 'User-Agent': 'Auto-Claude-UI', - 'Accept': 'application/vnd.github+json' - }; - - const request = https.get(url, { headers }, (response) => { - // Handle redirects - if (response.statusCode === 301 || response.statusCode === 302) { - const redirectUrl = response.headers.location; - if (redirectUrl) { - fetchJson(redirectUrl).then(resolve).catch(reject); - return; - } - } - - // Handle HTTP 300 Multiple Choices (branch/tag name collision) - if (response.statusCode === 300) { - let data = ''; - response.on('data', chunk => data += chunk); - response.on('end', () => { - console.error('[HTTP] Multiple choices for resource:', { - url, - statusCode: 300, - response: data - }); - reject(new Error( - `Multiple resources found for ${url}. ` + - `This usually means a branch and tag have the same name. ` + - `Please report this issue at https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/issues` - )); - }); - response.on('error', reject); - return; - } - - if (response.statusCode !== 200) { - // Collect response body for error details (limit to 10KB) - const maxErrorSize = 10 * 1024; - let errorData = ''; - response.on('data', chunk => { - if (errorData.length < maxErrorSize) { - errorData += chunk.toString().slice(0, maxErrorSize - errorData.length); - } - }); - response.on('end', () => { - const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`; - reject(new Error(errorMsg)); - }); - response.on('error', reject); - return; - } - - let data = ''; - response.on('data', chunk => data += chunk); - response.on('end', () => { - try { - resolve(JSON.parse(data) as T); - } catch (_e) { - reject(new Error('Failed to parse JSON response')); - } - }); - response.on('error', reject); - }); - - request.on('error', reject); - request.setTimeout(TIMEOUTS.requestTimeout, () => { - request.destroy(); - reject(new Error('Request timeout')); - }); - }); -} - -/** - * Download a file with progress tracking - */ -export function downloadFile( - url: string, - destPath: string, - onProgress?: (percent: number) => void -): Promise { - return new Promise((resolve, reject) => { - const file = createWriteStream(destPath); - - // GitHub API URLs need the GitHub Accept header to get a redirect to the actual file - // Non-API URLs (CDN, direct downloads) use octet-stream - const isGitHubApi = url.includes('api.github.com'); - const headers = { - 'User-Agent': 'Auto-Claude-UI', - 'Accept': isGitHubApi ? 'application/vnd.github+json' : 'application/octet-stream' - }; - - const request = https.get(url, { headers }, (response) => { - // Handle redirects - if (response.statusCode === 301 || response.statusCode === 302) { - file.close(); - const redirectUrl = response.headers.location; - if (redirectUrl) { - downloadFile(redirectUrl, destPath, onProgress).then(resolve).catch(reject); - return; - } - } - - // Handle HTTP 300 Multiple Choices (branch/tag name collision) - if (response.statusCode === 300) { - file.close(); - let data = ''; - response.on('data', chunk => data += chunk); - response.on('end', () => { - console.error('[HTTP] Multiple choices for resource:', { - url, - statusCode: 300, - response: data - }); - reject(new Error( - `Multiple resources found for ${url}. ` + - `This usually means a branch and tag have the same name. ` + - `Please download the latest version manually from: ` + - `https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest` - )); - }); - response.on('error', reject); - return; - } - - if (response.statusCode !== 200) { - file.close(); - // Collect response body for error details (limit to 10KB) - const maxErrorSize = 10 * 1024; - let errorData = ''; - response.on('data', chunk => { - if (errorData.length < maxErrorSize) { - errorData += chunk.toString().slice(0, maxErrorSize - errorData.length); - } - }); - response.on('end', () => { - const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`; - reject(new Error(errorMsg)); - }); - response.on('error', reject); - return; - } - - const totalSize = parseInt(response.headers['content-length'] || '0', 10); - let downloadedSize = 0; - - response.on('data', (chunk) => { - downloadedSize += chunk.length; - if (totalSize > 0 && onProgress) { - onProgress(Math.round((downloadedSize / totalSize) * 100)); - } - }); - - response.pipe(file); - - file.on('finish', () => { - file.close(); - resolve(); - }); - - file.on('error', (err) => { - file.close(); - reject(err); - }); - }); - - request.on('error', (err) => { - file.close(); - reject(err); - }); - - request.setTimeout(TIMEOUTS.downloadTimeout, () => { - request.destroy(); - reject(new Error('Download timeout')); - }); - }); -} diff --git a/apps/frontend/src/main/updater/types.ts b/apps/frontend/src/main/updater/types.ts deleted file mode 100644 index d1e0b2c5..00000000 --- a/apps/frontend/src/main/updater/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Type definitions for Auto Claude updater system - */ - -/** - * GitHub Release API response (partial) - */ -export interface GitHubRelease { - tag_name: string; - name: string; - body: string; - html_url: string; - tarball_url: string; - published_at: string; - prerelease: boolean; - draft: boolean; -} - -/** - * Result of checking for updates - */ -export interface AutoBuildUpdateCheck { - updateAvailable: boolean; - currentVersion: string; - latestVersion?: string; - releaseNotes?: string; - releaseUrl?: string; - error?: string; -} - -/** - * Result of applying an update - */ -export interface AutoBuildUpdateResult { - success: boolean; - version?: string; - error?: string; -} - -/** - * Update progress stages - */ -export type UpdateStage = 'checking' | 'downloading' | 'extracting' | 'complete' | 'error'; - -/** - * Progress callback for download - */ -export type UpdateProgressCallback = (progress: { - stage: UpdateStage; - percent?: number; - message: string; -}) => void; - -/** - * Update metadata stored after successful update - */ -export interface UpdateMetadata { - version: string; - updatedAt: string; - source: string; - releaseTag: string; - releaseName: string; -} diff --git a/apps/frontend/src/main/updater/update-checker.ts b/apps/frontend/src/main/updater/update-checker.ts deleted file mode 100644 index 2f04d933..00000000 --- a/apps/frontend/src/main/updater/update-checker.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Update checking functionality - */ - -import { GITHUB_CONFIG } from './config'; -import { fetchJson } from './http-client'; -import { getEffectiveVersion, parseVersionFromTag, compareVersions } from './version-manager'; -import { GitHubRelease, AutoBuildUpdateCheck } from './types'; -import { debugLog } from '../../shared/utils/debug-logger'; - -// Cache for the latest release info (used by download) -let cachedLatestRelease: GitHubRelease | null = null; - -/** - * Get cached release (if available) - */ -export function getCachedRelease(): GitHubRelease | null { - return cachedLatestRelease; -} - -/** - * Set cached release - */ -export function setCachedRelease(release: GitHubRelease | null): void { - cachedLatestRelease = release; -} - -/** - * Clear cached release - */ -export function clearCachedRelease(): void { - cachedLatestRelease = null; -} - -/** - * Check GitHub Releases for the latest version - */ -export async function checkForUpdates(): Promise { - // Use effective version which accounts for source updates - const currentVersion = getEffectiveVersion(); - debugLog('[UpdateCheck] Current effective version:', currentVersion); - - try { - // Fetch latest release from GitHub Releases API - const releaseUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`; - const release = await fetchJson(releaseUrl); - - // Cache for download function - setCachedRelease(release); - - // Parse version from tag (e.g., "v1.2.0" -> "1.2.0") - const latestVersion = parseVersionFromTag(release.tag_name); - debugLog('[UpdateCheck] Latest version:', latestVersion); - - // Compare versions - const updateAvailable = compareVersions(latestVersion, currentVersion) > 0; - debugLog('[UpdateCheck] Update available:', updateAvailable); - - return { - updateAvailable, - currentVersion, - latestVersion, - releaseNotes: release.body || undefined, - releaseUrl: release.html_url || undefined - }; - } catch (error) { - // Clear cache on error - clearCachedRelease(); - debugLog('[UpdateCheck] Error:', error instanceof Error ? error.message : error); - - return { - updateAvailable: false, - currentVersion, - error: error instanceof Error ? error.message : 'Failed to check for updates' - }; - } -} diff --git a/apps/frontend/src/main/updater/update-installer.ts b/apps/frontend/src/main/updater/update-installer.ts deleted file mode 100644 index a4e2d350..00000000 --- a/apps/frontend/src/main/updater/update-installer.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Update installation and application - */ - -import { existsSync, mkdirSync, writeFileSync, rmSync, readdirSync } from 'fs'; -import path from 'path'; -import { app } from 'electron'; -import { GITHUB_CONFIG, PRESERVE_FILES } from './config'; -import { downloadFile, fetchJson } from './http-client'; -import { parseVersionFromTag } from './version-manager'; -import { getUpdateCachePath, getUpdateTargetPath } from './path-resolver'; -import { extractTarball, copyDirectoryRecursive, preserveFiles, restoreFiles, cleanTargetDirectory } from './file-operations'; -import { getCachedRelease, setCachedRelease, clearCachedRelease } from './update-checker'; -import { GitHubRelease, AutoBuildUpdateResult, UpdateProgressCallback, UpdateMetadata } from './types'; -import { debugLog } from '../../shared/utils/debug-logger'; - -/** - * Download and apply the latest auto-claude update from GitHub Releases - * - * Note: In production, this updates the bundled source in userData. - * For packaged apps, we can't modify resourcesPath directly, - * so we use a "source override" system. - */ -export async function downloadAndApplyUpdate( - onProgress?: UpdateProgressCallback -): Promise { - const cachePath = getUpdateCachePath(); - - debugLog('[Update] Starting update process...'); - debugLog('[Update] Cache path:', cachePath); - - try { - onProgress?.({ - stage: 'checking', - message: 'Fetching release info...' - }); - - // Ensure cache directory exists - if (!existsSync(cachePath)) { - mkdirSync(cachePath, { recursive: true }); - debugLog('[Update] Created cache directory'); - } - - // Get release info (use cache or fetch fresh) - let release = getCachedRelease(); - if (!release) { - const releaseUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`; - debugLog('[Update] Fetching release info from:', releaseUrl); - release = await fetchJson(releaseUrl); - setCachedRelease(release); - } else { - debugLog('[Update] Using cached release info'); - } - - // Use explicit tag reference URL to avoid HTTP 300 when branch/tag names collide - // See: https://github.com/AndyMik90/Auto-Claude/issues/78 - const tarballUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tarball/refs/tags/${release.tag_name}`; - const releaseVersion = parseVersionFromTag(release.tag_name); - debugLog('[Update] Release version:', releaseVersion); - debugLog('[Update] Tarball URL:', tarballUrl); - - const tarballPath = path.join(cachePath, 'auto-claude-update.tar.gz'); - const extractPath = path.join(cachePath, 'extracted'); - - // Clean up previous extraction - if (existsSync(extractPath)) { - rmSync(extractPath, { recursive: true, force: true }); - } - mkdirSync(extractPath, { recursive: true }); - - onProgress?.({ - stage: 'downloading', - percent: 0, - message: 'Downloading update...' - }); - - debugLog('[Update] Starting download to:', tarballPath); - - // Download the tarball - await downloadFile(tarballUrl, tarballPath, (percent) => { - onProgress?.({ - stage: 'downloading', - percent, - message: `Downloading... ${percent}%` - }); - }); - - debugLog('[Update] Download complete'); - - onProgress?.({ - stage: 'extracting', - message: 'Extracting update...' - }); - - debugLog('[Update] Extracting to:', extractPath); - - // Extract the tarball - await extractTarball(tarballPath, extractPath); - - debugLog('[Update] Extraction complete'); - - // Find the auto-claude folder in extracted content - // GitHub tarballs have a root folder like "owner-repo-hash/" - const extractedDirs = readdirSync(extractPath); - if (extractedDirs.length === 0) { - throw new Error('Empty tarball'); - } - - const rootDir = path.join(extractPath, extractedDirs[0]); - const autoBuildSource = path.join(rootDir, GITHUB_CONFIG.autoBuildPath); - - if (!existsSync(autoBuildSource)) { - throw new Error('auto-claude folder not found in download'); - } - - // Determine where to install the update - const targetPath = getUpdateTargetPath(); - debugLog('[Update] Target install path:', targetPath); - - // Backup existing source (if in dev mode) - const backupPath = path.join(cachePath, 'backup'); - if (!app.isPackaged && existsSync(targetPath)) { - if (existsSync(backupPath)) { - rmSync(backupPath, { recursive: true, force: true }); - } - // Simple copy for backup - debugLog('[Update] Creating backup at:', backupPath); - copyDirectoryRecursive(targetPath, backupPath); - } - - // Apply the update - debugLog('[Update] Applying update...'); - await applyUpdate(targetPath, autoBuildSource); - debugLog('[Update] Update applied successfully'); - - // Write update metadata - const metadata: UpdateMetadata = { - version: releaseVersion, - updatedAt: new Date().toISOString(), - source: 'github-release', - releaseTag: release.tag_name, - releaseName: release.name - }; - writeUpdateMetadata(targetPath, metadata); - - // Clear the cache after successful update - clearCachedRelease(); - - // Cleanup - rmSync(tarballPath, { force: true }); - rmSync(extractPath, { recursive: true, force: true }); - - onProgress?.({ - stage: 'complete', - message: `Updated to version ${releaseVersion}` - }); - - debugLog('[Update] ============================================'); - debugLog('[Update] UPDATE SUCCESSFUL'); - debugLog('[Update] New version:', releaseVersion); - debugLog('[Update] Target path:', targetPath); - debugLog('[Update] ============================================'); - - return { - success: true, - version: releaseVersion - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Update failed'; - debugLog('[Update] ============================================'); - debugLog('[Update] UPDATE FAILED'); - debugLog('[Update] Error:', errorMessage); - debugLog('[Update] ============================================'); - - // Provide user-friendly error message for HTTP 300 errors - let displayMessage = errorMessage; - if (errorMessage.includes('Multiple resources found')) { - displayMessage = - `Update failed due to repository configuration issue (HTTP 300). ` + - `Please download the latest version manually from: ` + - `https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`; - } - - onProgress?.({ - stage: 'error', - message: displayMessage - }); - - return { - success: false, - error: displayMessage - }; - } -} - -/** - * Apply update to target directory - */ -async function applyUpdate(targetPath: string, sourcePath: string): Promise { - if (existsSync(targetPath)) { - // Preserve important files - const preservedContent = preserveFiles(targetPath, PRESERVE_FILES); - - // Clean target but preserve certain files - cleanTargetDirectory(targetPath, PRESERVE_FILES); - - // Copy new files - copyDirectoryRecursive(sourcePath, targetPath, true); - - // Restore preserved files that might have been overwritten - restoreFiles(targetPath, preservedContent); - } else { - mkdirSync(targetPath, { recursive: true }); - copyDirectoryRecursive(sourcePath, targetPath, false); - } -} - -/** - * Write update metadata to disk - */ -function writeUpdateMetadata(targetPath: string, metadata: UpdateMetadata): void { - const metadataPath = path.join(targetPath, '.update-metadata.json'); - writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); -} diff --git a/apps/frontend/src/main/updater/update-status.ts b/apps/frontend/src/main/updater/update-status.ts deleted file mode 100644 index 93ec5e29..00000000 --- a/apps/frontend/src/main/updater/update-status.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Update status checking utilities - */ - -import { existsSync, readFileSync } from 'fs'; -import path from 'path'; -import { app } from 'electron'; -import { getBundledVersion, compareVersions } from './version-manager'; -import { UpdateMetadata } from './types'; - -/** - * Check if there's a pending source update that requires restart - */ -export function hasPendingSourceUpdate(): boolean { - if (!app.isPackaged) { - return false; - } - - const overridePath = path.join(app.getPath('userData'), 'auto-claude-source'); - const metadataPath = path.join(overridePath, '.update-metadata.json'); - - if (!existsSync(metadataPath)) { - return false; - } - - try { - const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata; - const bundledVersion = getBundledVersion(); - return compareVersions(metadata.version, bundledVersion) > 0; - } catch { - return false; - } -} - -/** - * Get update metadata if available - */ -export function getUpdateMetadata(): UpdateMetadata | null { - const overridePath = path.join(app.getPath('userData'), 'auto-claude-source'); - const metadataPath = path.join(overridePath, '.update-metadata.json'); - - if (!existsSync(metadataPath)) { - return null; - } - - try { - return JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata; - } catch { - return null; - } -} diff --git a/apps/frontend/src/main/updater/version-manager.ts b/apps/frontend/src/main/updater/version-manager.ts index 92edcb8b..0924bd7e 100644 --- a/apps/frontend/src/main/updater/version-manager.ts +++ b/apps/frontend/src/main/updater/version-manager.ts @@ -1,96 +1,22 @@ /** * Version management utilities + * + * Simplified version that uses only the bundled app version. + * The "source updater" system has been removed since the backend + * is bundled with the app and updates via electron-updater. */ import { app } from 'electron'; -import { existsSync, readFileSync } from 'fs'; -import path from 'path'; -import type { UpdateMetadata } from './types'; /** * Get the current app/framework version from package.json * - * Uses app.getVersion() (from package.json) as the base version. + * Uses app.getVersion() (from package.json) as the version. */ export function getBundledVersion(): string { return app.getVersion(); } -/** - * Get the effective version - accounts for source updates - * - * Returns the updated source version if an update has been applied, - * otherwise returns the bundled version. - */ -export function getEffectiveVersion(): string { - const isDebug = process.env.DEBUG === 'true'; - - // Build list of paths to check for update metadata - const metadataPaths: string[] = []; - - if (app.isPackaged) { - // Production: check userData override path - metadataPaths.push( - path.join(app.getPath('userData'), 'auto-claude-source', '.update-metadata.json') - ); - } else { - // Development: check the actual source paths where updates are written - const possibleSourcePaths = [ - // Apps structure: apps/backend - path.join(app.getAppPath(), '..', 'backend'), - path.join(process.cwd(), 'apps', 'backend'), - path.resolve(__dirname, '..', '..', '..', 'backend') - ]; - - for (const sourcePath of possibleSourcePaths) { - metadataPaths.push(path.join(sourcePath, '.update-metadata.json')); - } - } - - if (isDebug) { - console.log('[Version] Checking metadata paths:', metadataPaths); - } - - // Check each path for metadata - for (const metadataPath of metadataPaths) { - const exists = existsSync(metadataPath); - if (isDebug) { - console.log(`[Version] Checking ${metadataPath}: ${exists ? 'EXISTS' : 'not found'}`); - } - if (exists) { - try { - const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as UpdateMetadata; - if (metadata.version) { - if (isDebug) { - console.log(`[Version] Found metadata version: ${metadata.version}`); - } - return metadata.version; - } - } catch (e) { - if (isDebug) { - console.log(`[Version] Error reading metadata: ${e}`); - } - // Continue to next path - } - } - } - - const bundledVersion = app.getVersion(); - if (isDebug) { - console.log(`[Version] No metadata found, using bundled version: ${bundledVersion}`); - } - return bundledVersion; -} - -/** - * Parse version from GitHub release tag - * Handles tags like "v1.2.0", "1.2.0", "v1.2.0-beta" - */ -export function parseVersionFromTag(tag: string): string { - // Remove leading 'v' if present - return tag.replace(/^v/, ''); -} - /** * Parse a version string into its components * Handles versions like "2.7.2", "2.7.2-beta.6", "2.7.2-alpha.1" diff --git a/apps/frontend/src/preload/api/agent-api.ts b/apps/frontend/src/preload/api/agent-api.ts index c4ae68ff..f9af4fad 100644 --- a/apps/frontend/src/preload/api/agent-api.ts +++ b/apps/frontend/src/preload/api/agent-api.ts @@ -8,7 +8,6 @@ * - Changelog operations * - Linear integration * - GitHub integration - * - Auto-build source updates * - Shell operations */ @@ -19,7 +18,6 @@ import { createChangelogAPI, ChangelogAPI } from './modules/changelog-api'; import { createLinearAPI, LinearAPI } from './modules/linear-api'; import { createGitHubAPI, GitHubAPI } from './modules/github-api'; import { createGitLabAPI, GitLabAPI } from './modules/gitlab-api'; -import { createAutoBuildAPI, AutoBuildAPI } from './modules/autobuild-api'; import { createShellAPI, ShellAPI } from './modules/shell-api'; /** @@ -34,7 +32,6 @@ export interface AgentAPI extends LinearAPI, GitHubAPI, GitLabAPI, - AutoBuildAPI, ShellAPI {} /** @@ -50,7 +47,6 @@ export const createAgentAPI = (): AgentAPI => { const linearAPI = createLinearAPI(); const githubAPI = createGitHubAPI(); const gitlabAPI = createGitLabAPI(); - const autobuildAPI = createAutoBuildAPI(); const shellAPI = createShellAPI(); return { @@ -75,9 +71,6 @@ export const createAgentAPI = (): AgentAPI => { // GitLab Integration API ...gitlabAPI, - // Auto-Build Source Update API - ...autobuildAPI, - // Shell Operations API ...shellAPI }; @@ -92,6 +85,5 @@ export type { LinearAPI, GitHubAPI, GitLabAPI, - AutoBuildAPI, ShellAPI }; diff --git a/apps/frontend/src/preload/api/app-update-api.ts b/apps/frontend/src/preload/api/app-update-api.ts index 3ba20af7..313c16ed 100644 --- a/apps/frontend/src/preload/api/app-update-api.ts +++ b/apps/frontend/src/preload/api/app-update-api.ts @@ -16,6 +16,7 @@ export interface AppUpdateAPI { // Operations checkAppUpdate: () => Promise>; downloadAppUpdate: () => Promise; + downloadStableUpdate: () => Promise; installAppUpdate: () => void; getAppVersion: () => Promise; @@ -29,6 +30,9 @@ export interface AppUpdateAPI { onAppUpdateProgress: ( callback: (progress: AppUpdateProgress) => void ) => IpcListenerCleanup; + onAppUpdateStableDowngrade: ( + callback: (info: AppUpdateInfo) => void + ) => IpcListenerCleanup; } /** @@ -42,6 +46,9 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({ downloadAppUpdate: (): Promise => invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD), + downloadStableUpdate: (): Promise => + invokeIpc(IPC_CHANNELS.APP_UPDATE_DOWNLOAD_STABLE), + installAppUpdate: (): void => { invokeIpc(IPC_CHANNELS.APP_UPDATE_INSTALL); }, @@ -63,5 +70,10 @@ export const createAppUpdateAPI = (): AppUpdateAPI => ({ onAppUpdateProgress: ( callback: (progress: AppUpdateProgress) => void ): IpcListenerCleanup => - createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback) + createIpcListener(IPC_CHANNELS.APP_UPDATE_PROGRESS, callback), + + onAppUpdateStableDowngrade: ( + callback: (info: AppUpdateInfo) => void + ): IpcListenerCleanup => + createIpcListener(IPC_CHANNELS.APP_UPDATE_STABLE_DOWNGRADE, callback) }); diff --git a/apps/frontend/src/preload/api/modules/autobuild-api.ts b/apps/frontend/src/preload/api/modules/autobuild-api.ts deleted file mode 100644 index e0e7aca6..00000000 --- a/apps/frontend/src/preload/api/modules/autobuild-api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IPC_CHANNELS } from '../../../shared/constants'; -import type { - AutoBuildSourceUpdateCheck, - AutoBuildSourceUpdateProgress, - IPCResult -} from '../../../shared/types'; -import { createIpcListener, invokeIpc, sendIpc, IpcListenerCleanup } from './ipc-utils'; - -/** - * Auto-Build Source Update API operations - */ -export interface AutoBuildAPI { - // Operations - checkAutoBuildSourceUpdate: () => Promise>; - downloadAutoBuildSourceUpdate: () => void; - getAutoBuildSourceVersion: () => Promise>; - - // Event Listeners - onAutoBuildSourceUpdateProgress: ( - callback: (progress: AutoBuildSourceUpdateProgress) => void - ) => IpcListenerCleanup; -} - -/** - * Creates the Auto-Build Source Update API implementation - */ -export const createAutoBuildAPI = (): AutoBuildAPI => ({ - // Operations - checkAutoBuildSourceUpdate: (): Promise> => - invokeIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_CHECK), - - downloadAutoBuildSourceUpdate: (): void => - sendIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_DOWNLOAD), - - getAutoBuildSourceVersion: (): Promise> => - invokeIpc(IPC_CHANNELS.AUTOBUILD_SOURCE_VERSION), - - // Event Listeners - onAutoBuildSourceUpdateProgress: ( - callback: (progress: AutoBuildSourceUpdateProgress) => void - ): IpcListenerCleanup => - createIpcListener(IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, callback) -}); diff --git a/apps/frontend/src/preload/api/modules/index.ts b/apps/frontend/src/preload/api/modules/index.ts index 48b4f8b2..e2cc5537 100644 --- a/apps/frontend/src/preload/api/modules/index.ts +++ b/apps/frontend/src/preload/api/modules/index.ts @@ -11,6 +11,5 @@ export * from './insights-api'; export * from './changelog-api'; export * from './linear-api'; export * from './github-api'; -export * from './autobuild-api'; export * from './shell-api'; export * from './debug-api'; diff --git a/apps/frontend/src/renderer/components/settings/AdvancedSettings.tsx b/apps/frontend/src/renderer/components/settings/AdvancedSettings.tsx index 74efdddf..5b58abe2 100644 --- a/apps/frontend/src/renderer/components/settings/AdvancedSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/AdvancedSettings.tsx @@ -3,25 +3,21 @@ import { useTranslation } from 'react-i18next'; import { RefreshCw, CheckCircle2, - AlertCircle, - CloudDownload, - Loader2, - ExternalLink, Download, - Sparkles + Sparkles, + ArrowDownToLine, + X } from 'lucide-react'; import { Button } from '../ui/button'; import { Label } from '../ui/label'; import { Switch } from '../ui/switch'; import { Progress } from '../ui/progress'; -import { cn } from '../../lib/utils'; import { SettingsSection } from './SettingsSection'; import type { AppSettings, - AutoBuildSourceUpdateCheck, - AutoBuildSourceUpdateProgress, AppUpdateAvailableEvent, AppUpdateProgress, + AppUpdateInfo, NotificationSettings } from '../../../shared/types'; @@ -75,53 +71,22 @@ interface AdvancedSettingsProps { export function AdvancedSettings({ settings, onSettingsChange, section, version }: AdvancedSettingsProps) { const { t } = useTranslation('settings'); - // Auto Claude source update state - const [sourceUpdateCheck, setSourceUpdateCheck] = useState(null); - const [isCheckingSourceUpdate, setIsCheckingSourceUpdate] = useState(false); - const [isDownloadingUpdate, setIsDownloadingUpdate] = useState(false); - const [downloadProgress, setDownloadProgress] = useState(null); - // Local version state that can be updated after successful update - const [displayVersion, setDisplayVersion] = useState(version); - // Electron app update state const [appUpdateInfo, setAppUpdateInfo] = useState(null); - const [_isCheckingAppUpdate, setIsCheckingAppUpdate] = useState(false); + const [isCheckingAppUpdate, setIsCheckingAppUpdate] = useState(false); const [isDownloadingAppUpdate, setIsDownloadingAppUpdate] = useState(false); const [appDownloadProgress, setAppDownloadProgress] = useState(null); const [isAppUpdateDownloaded, setIsAppUpdateDownloaded] = useState(false); - - // Sync displayVersion with prop when it changes - useEffect(() => { - setDisplayVersion(version); - }, [version]); + // Stable downgrade state (shown when user turns off beta while on prerelease) + const [stableDowngradeInfo, setStableDowngradeInfo] = useState(null); // Check for updates on mount useEffect(() => { if (section === 'updates') { - checkForSourceUpdates(); checkForAppUpdates(); } }, [section]); - // Listen for source download progress - useEffect(() => { - const cleanup = window.electronAPI.onAutoBuildSourceUpdateProgress((progress) => { - setDownloadProgress(progress); - if (progress.stage === 'complete') { - setIsDownloadingUpdate(false); - // Update the displayed version if a new version was provided - if (progress.newVersion) { - setDisplayVersion(progress.newVersion); - } - checkForSourceUpdates(); - } else if (progress.stage === 'error') { - setIsDownloadingUpdate(false); - } - }); - - return cleanup; - }, []); - // Listen for app update events useEffect(() => { const cleanupAvailable = window.electronAPI.onAppUpdateAvailable((info) => { @@ -134,16 +99,24 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version setIsDownloadingAppUpdate(false); setIsAppUpdateDownloaded(true); setAppDownloadProgress(null); + // Clear downgrade info if any update downloaded + setStableDowngradeInfo(null); }); const cleanupProgress = window.electronAPI.onAppUpdateProgress((progress) => { setAppDownloadProgress(progress); }); + // Listen for stable downgrade available (when user turns off beta while on prerelease) + const cleanupStableDowngrade = window.electronAPI.onAppUpdateStableDowngrade((info) => { + setStableDowngradeInfo(info); + }); + return () => { cleanupAvailable(); cleanupDownloaded(); cleanupProgress(); + cleanupStableDowngrade(); }; }, []); @@ -167,7 +140,12 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version const handleDownloadAppUpdate = async () => { setIsDownloadingAppUpdate(true); try { - await window.electronAPI.downloadAppUpdate(); + const result = await window.electronAPI.downloadAppUpdate(); + if (!result.success) { + console.error('Failed to download app update:', result.error); + setIsDownloadingAppUpdate(false); + } + // Note: Success case is handled by the onAppUpdateDownloaded event listener } catch (err) { console.error('Failed to download app update:', err); setIsDownloadingAppUpdate(false); @@ -178,30 +156,24 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version window.electronAPI.installAppUpdate(); }; - const checkForSourceUpdates = async () => { - console.log('[AdvancedSettings] Checking for source updates...'); - setIsCheckingSourceUpdate(true); + const handleDownloadStableVersion = async () => { + setIsDownloadingAppUpdate(true); try { - const result = await window.electronAPI.checkAutoBuildSourceUpdate(); - console.log('[AdvancedSettings] Check result:', result); - if (result.success && result.data) { - setSourceUpdateCheck(result.data); - // Update displayed version from the check result (most accurate) - if (result.data.currentVersion) { - setDisplayVersion(result.data.currentVersion); - } + // Use dedicated stable download API with allowDowngrade enabled + const result = await window.electronAPI.downloadStableUpdate(); + if (!result.success) { + console.error('Failed to download stable version:', result.error); + setIsDownloadingAppUpdate(false); } + // Note: Success case is handled by the onAppUpdateDownloaded event listener } catch (err) { - console.error('[AdvancedSettings] Check error:', err); - } finally { - setIsCheckingSourceUpdate(false); + console.error('Failed to download stable version:', err); + setIsDownloadingAppUpdate(false); } }; - const handleDownloadSourceUpdate = () => { - setIsDownloadingUpdate(true); - setDownloadProgress(null); - window.electronAPI.downloadAutoBuildSourceUpdate(); + const dismissStableDowngrade = () => { + setStableDowngradeInfo(null); }; if (section === 'updates') { @@ -211,7 +183,45 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version description={t('updates.description')} >
- {/* Electron App Update Section */} + {/* Current Version Display */} +
+
+
+

{t('updates.version')}

+

+ {version || t('updates.loading')} +

+
+ {isCheckingAppUpdate ? ( + + ) : appUpdateInfo ? ( + + ) : ( + + )} +
+ + {/* Update status */} + {!appUpdateInfo && !isCheckingAppUpdate && ( +

+ {t('updates.latestVersion')} +

+ )} + +
+ +
+
+ + {/* Electron App Update Section - shows when update available */} {(appUpdateInfo || isAppUpdateDownloaded) && (
@@ -302,113 +312,6 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
)} - {/* Unified Version Display with Update Check */} -
-
-
-

{t('updates.version')}

-

- {displayVersion || t('updates.loading')} -

-
- {isCheckingSourceUpdate ? ( - - ) : sourceUpdateCheck?.updateAvailable ? ( - - ) : ( - - )} -
- - {/* Update status */} - {isCheckingSourceUpdate ? ( -

- {t('updates.checkingForUpdates')} -

- ) : sourceUpdateCheck ? ( - <> - {sourceUpdateCheck.latestVersion && sourceUpdateCheck.updateAvailable && ( -

- {t('updates.newVersionAvailable')} {sourceUpdateCheck.latestVersion} -

- )} - - {sourceUpdateCheck.error && ( -

{sourceUpdateCheck.error}

- )} - - {!sourceUpdateCheck.updateAvailable && !sourceUpdateCheck.error && ( -

- {t('updates.latestVersion')} -

- )} - - {sourceUpdateCheck.updateAvailable && ( -
- {sourceUpdateCheck.releaseNotes && ( -
- -
- )} - - {sourceUpdateCheck.releaseUrl && ( - - )} - - {isDownloadingUpdate ? ( -
-
- - {downloadProgress?.message || 'Downloading...'} -
- {downloadProgress?.percent !== undefined && ( - - )} -
- ) : downloadProgress?.stage === 'complete' ? ( -
- - {downloadProgress.message} -
- ) : downloadProgress?.stage === 'error' ? ( -
- - {downloadProgress.message} -
- ) : ( - - )} -
- )} - - ) : ( -

- {t('updates.unableToCheck')} -

- )} - -
- -
-
-
@@ -433,11 +336,113 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version
- onSettingsChange({ ...settings, betaUpdates: checked }) - } + onCheckedChange={(checked) => { + onSettingsChange({ ...settings, betaUpdates: checked }); + if (checked) { + // Clear downgrade info when enabling beta again + setStableDowngradeInfo(null); + } else { + // Clear beta update info when disabling beta, so stable downgrade UI can show + setAppUpdateInfo(null); + } + }} />
+ + {/* Stable Downgrade Section - shown when user turns off beta while on prerelease */} + {stableDowngradeInfo && !appUpdateInfo && ( +
+
+
+ +

{t('updates.stableDowngradeAvailable')}

+
+ +
+ +

+ {t('updates.stableDowngradeDescription')} +

+ +
+
+

+ {t('updates.stableVersion')} +

+

+ {stableDowngradeInfo.version} +

+ {stableDowngradeInfo.releaseDate && ( +

+ {t('updates.released')} {new Date(stableDowngradeInfo.releaseDate).toLocaleDateString()} +

+ )} +
+ {isDownloadingAppUpdate ? ( + + ) : ( + + )} +
+ + {/* Release Notes */} + {stableDowngradeInfo.releaseNotes && ( +
+ +
+ )} + + {/* Download Progress */} + {isDownloadingAppUpdate && appDownloadProgress && ( +
+
+ {t('updates.downloading')} + + {Math.round(appDownloadProgress.percent)}% + +
+ +

+ {(appDownloadProgress.transferred / 1024 / 1024).toFixed(2)} MB / {(appDownloadProgress.total / 1024 / 1024).toFixed(2)} MB +

+
+ )} + + {/* Action Buttons */} +
+ + +
+
+ )}
); diff --git a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts index 057daf94..81168fa0 100644 --- a/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/infrastructure-mock.ts @@ -174,28 +174,6 @@ export const infrastructureMock = { onIdeationTypeComplete: () => () => {}, onIdeationTypeFailed: () => () => {}, - // Auto-Build Source Update Operations - checkAutoBuildSourceUpdate: async () => ({ - success: true, - data: { - updateAvailable: true, - currentVersion: '1.0.0', - latestVersion: '1.1.0', - releaseNotes: '## v1.1.0\n\n- New feature: Enhanced spec creation\n- Bug fix: Improved error handling\n- Performance improvements' - } - }), - - downloadAutoBuildSourceUpdate: () => { - console.warn('[Browser Mock] downloadAutoBuildSourceUpdate called'); - }, - - getAutoBuildSourceVersion: async () => ({ - success: true, - data: '1.0.0' - }), - - onAutoBuildSourceUpdateProgress: () => () => {}, - // Shell Operations openExternal: async (url: string) => { console.warn('[Browser Mock] openExternal:', url); diff --git a/apps/frontend/src/renderer/lib/mocks/settings-mock.ts b/apps/frontend/src/renderer/lib/mocks/settings-mock.ts index 035789d6..97c83313 100644 --- a/apps/frontend/src/renderer/lib/mocks/settings-mock.ts +++ b/apps/frontend/src/renderer/lib/mocks/settings-mock.ts @@ -36,10 +36,12 @@ export const settingsMock = { // App Update Operations (mock - no updates in browser mode) checkAppUpdate: async () => ({ success: true, data: null }), downloadAppUpdate: async () => ({ success: true }), + downloadStableUpdate: async () => ({ success: true }), installAppUpdate: () => { console.warn('[browser-mock] installAppUpdate called'); }, // App Update Event Listeners (no-op in browser mode) onAppUpdateAvailable: () => () => {}, onAppUpdateDownloaded: () => () => {}, - onAppUpdateProgress: () => () => {} + onAppUpdateProgress: () => () => {}, + onAppUpdateStableDowngrade: () => () => {} }; diff --git a/apps/frontend/src/shared/constants/ipc.ts b/apps/frontend/src/shared/constants/ipc.ts index 3b369d33..6c244d6d 100644 --- a/apps/frontend/src/shared/constants/ipc.ts +++ b/apps/frontend/src/shared/constants/ipc.ts @@ -400,12 +400,6 @@ export const IPC_CHANNELS = { OLLAMA_PULL_MODEL: 'ollama:pullModel', OLLAMA_PULL_PROGRESS: 'ollama:pullProgress', - // Auto Claude source updates - AUTOBUILD_SOURCE_CHECK: 'autobuild:source:check', - AUTOBUILD_SOURCE_DOWNLOAD: 'autobuild:source:download', - AUTOBUILD_SOURCE_VERSION: 'autobuild:source:version', - AUTOBUILD_SOURCE_PROGRESS: 'autobuild:source:progress', - // Auto Claude source environment configuration AUTOBUILD_SOURCE_ENV_GET: 'autobuild:source:env:get', AUTOBUILD_SOURCE_ENV_UPDATE: 'autobuild:source:env:update', @@ -463,6 +457,7 @@ export const IPC_CHANNELS = { // App auto-update operations APP_UPDATE_CHECK: 'app-update:check', APP_UPDATE_DOWNLOAD: 'app-update:download', + APP_UPDATE_DOWNLOAD_STABLE: 'app-update:download-stable', // Download stable version (for downgrade from beta) APP_UPDATE_INSTALL: 'app-update:install', APP_UPDATE_GET_VERSION: 'app-update:get-version', @@ -471,6 +466,7 @@ export const IPC_CHANNELS = { APP_UPDATE_DOWNLOADED: 'app-update:downloaded', APP_UPDATE_PROGRESS: 'app-update:progress', APP_UPDATE_ERROR: 'app-update:error', + APP_UPDATE_STABLE_DOWNGRADE: 'app-update:stable-downgrade', // Stable version available for downgrade from beta // Release operations RELEASE_SUGGEST_VERSION: 'release:suggestVersion', diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index 03bc7767..c1399915 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -161,7 +161,11 @@ "autoUpdateProjects": "Auto-Update Projects", "autoUpdateProjectsDescription": "Automatically update Auto Claude in projects when a new version is available", "betaUpdates": "Beta Updates", - "betaUpdatesDescription": "Receive pre-release beta versions with new features (may be less stable)" + "betaUpdatesDescription": "Receive pre-release beta versions with new features (may be less stable)", + "stableDowngradeAvailable": "Stable Version Available", + "stableDowngradeDescription": "You're currently on a beta version. Since you've disabled beta updates, you can switch to the latest stable release.", + "stableVersion": "Stable Version", + "downloadStableVersion": "Download Stable Version" }, "notifications": { "title": "Notifications", diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 58954c84..dfe33ce6 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -161,7 +161,11 @@ "autoUpdateProjects": "Mise à jour automatique des projets", "autoUpdateProjectsDescription": "Mettre à jour automatiquement Auto Claude dans les projets quand une nouvelle version est disponible", "betaUpdates": "Mises à jour bêta", - "betaUpdatesDescription": "Recevoir les versions bêta pré-release avec de nouvelles fonctionnalités (peut être moins stable)" + "betaUpdatesDescription": "Recevoir les versions bêta pré-release avec de nouvelles fonctionnalités (peut être moins stable)", + "stableDowngradeAvailable": "Version stable disponible", + "stableDowngradeDescription": "Vous êtes actuellement sur une version bêta. Comme vous avez désactivé les mises à jour bêta, vous pouvez passer à la dernière version stable.", + "stableVersion": "Version stable", + "downloadStableVersion": "Télécharger la version stable" }, "notifications": { "title": "Notifications", diff --git a/apps/frontend/src/shared/types/ipc.ts b/apps/frontend/src/shared/types/ipc.ts index 3fc02ea0..c238d0db 100644 --- a/apps/frontend/src/shared/types/ipc.ts +++ b/apps/frontend/src/shared/types/ipc.ts @@ -62,7 +62,7 @@ import type { ClaudeAuthResult, ClaudeUsageSnapshot } from './agent'; -import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult, AutoBuildSourceUpdateCheck, AutoBuildSourceUpdateProgress } from './settings'; +import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult } from './settings'; import type { AppUpdateInfo, AppUpdateProgress, AppUpdateAvailableEvent, AppUpdateDownloadedEvent } from './app-update'; import type { ChangelogTask, @@ -567,19 +567,10 @@ export interface ElectronAPI { callback: (projectId: string, ideationType: string) => void ) => () => void; - // Auto Claude source update operations - checkAutoBuildSourceUpdate: () => Promise>; - downloadAutoBuildSourceUpdate: () => void; - getAutoBuildSourceVersion: () => Promise>; - - // Auto Claude source update event listeners - onAutoBuildSourceUpdateProgress: ( - callback: (progress: AutoBuildSourceUpdateProgress) => void - ) => () => void; - // Electron app update operations checkAppUpdate: () => Promise>; downloadAppUpdate: () => Promise; + downloadStableUpdate: () => Promise; installAppUpdate: () => void; // Electron app update event listeners @@ -592,6 +583,9 @@ export interface ElectronAPI { onAppUpdateProgress: ( callback: (progress: AppUpdateProgress) => void ) => () => void; + onAppUpdateStableDowngrade: ( + callback: (info: AppUpdateInfo) => void + ) => () => void; // Shell operations openExternal: (url: string) => Promise; diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index 6dc0eaa1..ac666304 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -294,27 +294,3 @@ export interface SourceEnvCheckResult { sourcePath?: string; error?: string; } - -// Auto Claude Source Update Types -export interface AutoBuildSourceUpdateCheck { - updateAvailable: boolean; - currentVersion: string; - latestVersion?: string; - releaseNotes?: string; - releaseUrl?: string; - error?: string; -} - -export interface AutoBuildSourceUpdateResult { - success: boolean; - version?: string; - error?: string; -} - -export interface AutoBuildSourceUpdateProgress { - stage: 'checking' | 'downloading' | 'extracting' | 'complete' | 'error'; - percent?: number; - message: string; - /** New version after successful update - used to refresh UI */ - newVersion?: string; -} diff --git a/guides/CLI-USAGE.md b/guides/CLI-USAGE.md index 13008a87..c0c50785 100644 --- a/guides/CLI-USAGE.md +++ b/guides/CLI-USAGE.md @@ -182,7 +182,35 @@ python validate_spec.py --spec-dir specs/001-feature --checkpoint all ## Environment Variables +Copy `.env.example` to `.env` and configure as needed: + +```bash +cp .env.example .env +``` + +### Core Settings + | Variable | Required | Description | |----------|----------|-------------| | `CLAUDE_CODE_OAUTH_TOKEN` | Yes | OAuth token from `claude setup-token` | | `AUTO_BUILD_MODEL` | No | Model override (default: claude-opus-4-5-20251101) | +| `DEFAULT_BRANCH` | No | Base branch for worktrees (auto-detects main/master) | +| `DEBUG` | No | Enable debug logging (default: false) | + +### Integrations + +| Variable | Required | Description | +|----------|----------|-------------| +| `LINEAR_API_KEY` | No | Linear API key for task sync | +| `GITLAB_TOKEN` | No | GitLab Personal Access Token | +| `GITLAB_INSTANCE_URL` | No | GitLab instance URL (defaults to gitlab.com) | + +### Memory Layer (Graphiti) + +| Variable | Required | Description | +|----------|----------|-------------| +| `GRAPHITI_ENABLED` | No | Enable Memory Layer (default: true) | +| `GRAPHITI_LLM_PROVIDER` | No | LLM provider: openai, anthropic, ollama, google, openrouter | +| `GRAPHITI_EMBEDDER_PROVIDER` | No | Embedder: openai, voyage, ollama, google, openrouter | + +See `.env.example` for complete configuration options including provider-specific settings. diff --git a/guides/linux.md b/guides/linux.md new file mode 100644 index 00000000..077179f5 --- /dev/null +++ b/guides/linux.md @@ -0,0 +1,95 @@ +# Linux Installation & Building Guide + +This guide covers Linux-specific installation options and building from source. + +## Flatpak Installation + +Flatpak packages are available for Linux users who prefer sandboxed applications. + +### Download Flatpak + +See the [main README](../README.md#beta-release) for Flatpak download links in the Beta Release section. + +### Building Flatpak from Source + +To build the Flatpak package yourself, you need additional dependencies: + +```bash +# Fedora/RHEL +sudo dnf install flatpak-builder + +# Ubuntu/Debian +sudo apt install flatpak-builder + +# Install required Flatpak runtimes +flatpak install flathub org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08 +flatpak install flathub org.electronjs.Electron2.BaseApp//25.08 + +# Build the Flatpak +cd apps/frontend +npm run package:flatpak +``` + +The Flatpak will be created in `apps/frontend/dist/`. + +### Installing the Built Flatpak + +After building, install the Flatpak locally: + +```bash +flatpak install --user apps/frontend/dist/Auto-Claude-*.flatpak +``` + +### Running from Flatpak + +```bash +flatpak run com.autoclaude.AutoClaude +``` + +## Other Linux Packages + +### AppImage + +AppImage files are portable and don't require installation: + +```bash +# Make executable +chmod +x Auto-Claude-*-linux-x86_64.AppImage + +# Run +./Auto-Claude-*-linux-x86_64.AppImage +``` + +### Debian Package (.deb) + +For Ubuntu/Debian systems: + +```bash +sudo dpkg -i Auto-Claude-*-linux-amd64.deb +``` + +## Troubleshooting + +### Flatpak Runtime Issues + +If you encounter runtime issues with Flatpak: + +```bash +# Update runtimes +flatpak update + +# Check for missing runtimes +flatpak list --runtime +``` + +### AppImage Not Starting + +If the AppImage doesn't start: + +```bash +# Check for missing libraries +ldd ./Auto-Claude-*-linux-x86_64.AppImage + +# Try running with debug output +./Auto-Claude-*-linux-x86_64.AppImage --verbose +```